88import logging
99import time
1010from collections .abc import Callable
11- from dataclasses import asdict
11+ from dataclasses import asdict , replace
1212from enum import Enum
1313from typing import Any
1414from unittest .mock import Mock
@@ -50,6 +50,70 @@ def _serialize_dataclass(obj: Any) -> dict[str, Any]:
5050 return {k : (v .value if isinstance (v , Enum ) else v ) for k , v in asdict (obj ).items () if v is not None }
5151
5252
53+ DEFAULT_STATUS = StatusV2 (
54+ msg_ver = 2 ,
55+ msg_seq = 458 ,
56+ state = RoborockStateCode .charging ,
57+ battery = 100 ,
58+ clean_time = 1176 ,
59+ clean_area = 20965000 ,
60+ error_code = RoborockErrorCode (0 ),
61+ map_present = 1 ,
62+ in_cleaning = RoborockInCleaning .complete ,
63+ in_returning = 0 ,
64+ in_fresh_state = 1 ,
65+ lab_status = 1 ,
66+ water_box_status = 1 ,
67+ back_type = - 1 ,
68+ wash_phase = 0 ,
69+ wash_ready = 0 ,
70+ fan_power = 102 ,
71+ dnd_enabled = 0 ,
72+ map_status = 3 ,
73+ is_locating = 0 ,
74+ lock_status = 0 ,
75+ water_box_mode = 200 ,
76+ water_box_carriage_status = 1 ,
77+ mop_forbidden_enable = 1 ,
78+ camera_status = 3457 ,
79+ is_exploring = 0 ,
80+ home_sec_status = 0 ,
81+ home_sec_enable_password = 0 ,
82+ adbumper_status = [0 , 0 , 0 ],
83+ water_shortage_status = 0 ,
84+ dock_type = RoborockDockTypeCode .s8_dock ,
85+ dust_collection_status = 0 ,
86+ auto_dust_collection = 1 ,
87+ avoid_count = 19 ,
88+ mop_mode = 300 ,
89+ debug_mode = 0 ,
90+ collision_avoid_status = 1 ,
91+ switch_map_mode = 0 ,
92+ dock_error_status = RoborockDockErrorCode (0 ),
93+ charge_status = RoborockChargeStatus .charge_waiting ,
94+ unsave_map_reason = 0 ,
95+ unsave_map_flag = 0 ,
96+ dss = 169 ,
97+ )
98+
99+ DEFAULT_APP_INIT = AppInitStatus (
100+ local_info = AppInitStatusLocalInfo (
101+ location = "us" ,
102+ bom = "A.03.0069" ,
103+ featureset = 1 ,
104+ language = "en" ,
105+ logserver = "awsusor0.fds.api.xiaomi.com" ,
106+ wifiplan = "0x39" ,
107+ timezone = "US/Pacific" ,
108+ name = "custom_A.03.0069_FCC" ,
109+ ),
110+ feature_info = [111 , 112 , 113 , 114 , 115 , 116 , 117 , 118 , 119 , 120 , 122 , 123 , 124 , 125 ],
111+ new_feature_info = 633887780925447 ,
112+ new_feature_info_str = "0000000000002000" ,
113+ new_feature_info_2 = 8192 ,
114+ )
115+
116+
53117class V1VacuumSimulator (RoborockDeviceSimulator ):
54118 """Firmware simulator for a V1/L01 vacuum device.
55119
@@ -66,7 +130,7 @@ def __init__(
66130 self ,
67131 duid : str = "fake_duid" ,
68132 battery : int = 100 ,
69- state : int = RoborockStateCode .charging ,
133+ state : RoborockStateCode | int = RoborockStateCode .charging ,
70134 fan_power : int = 102 , # balanced
71135 dnd_enabled : int = 0 ,
72136 mop_mode : int = 300 ,
@@ -75,18 +139,22 @@ def __init__(
75139 device_info : HomeDataDevice | None = None ,
76140 product : HomeDataProduct | None = None ,
77141 dss : int = 169 ,
78- dock_type : int = 3 ,
142+ dock_type : RoborockDockTypeCode | int = 3 ,
79143 ):
80144 super ().__init__ (duid = duid , device_info = device_info , product = product )
81- self .battery = battery
82- self .state = state
83- self .fan_power = fan_power
84- self .dnd_enabled = dnd_enabled
85- self .mop_mode = mop_mode
86- self .water_box_mode = water_box_mode
145+ self .status = replace (DEFAULT_STATUS )
146+ self .app_init = replace (DEFAULT_APP_INIT )
147+ self .app_init .local_info = replace (DEFAULT_APP_INIT .local_info )
148+
149+ self .status .battery = battery
150+ self .status .state = RoborockStateCode (state )
151+ self .status .fan_power = fan_power
152+ self .status .dnd_enabled = dnd_enabled
153+ self .status .mop_mode = mop_mode
154+ self .status .water_box_mode = water_box_mode
155+ self .status .dss = dss
156+ self .status .dock_type = RoborockDockTypeCode (dock_type )
87157 self .custom_handlers = custom_handlers or {}
88- self .dss = dss
89- self .dock_type = dock_type
90158
91159 self .network_info = NetworkInfo (
92160 ip = "1.1.1.1" ,
@@ -174,105 +242,143 @@ def v1_channel(self) -> V1Channel:
174242 """Returns the real V1Channel bound to the fake channels."""
175243 return self ._v1_channel
176244
245+ @property
246+ def battery (self ) -> int :
247+ """Get battery level."""
248+ return self .status .battery or 0
249+
250+ @battery .setter
251+ def battery (self , value : int ) -> None :
252+ """Set battery level."""
253+ self .status .battery = value
254+
255+ @property
256+ def state (self ) -> RoborockStateCode :
257+ """Get device state code."""
258+ return self .status .state or RoborockStateCode .charging
259+
260+ @state .setter
261+ def state (self , value : RoborockStateCode | int ) -> None :
262+ """Set device state code."""
263+ self .status .state = RoborockStateCode (value )
264+
265+ @property
266+ def fan_power (self ) -> int :
267+ """Get fan power speed."""
268+ return self .status .fan_power or 0
269+
270+ @fan_power .setter
271+ def fan_power (self , value : int ) -> None :
272+ """Set fan power speed."""
273+ self .status .fan_power = value
274+
275+ @property
276+ def dnd_enabled (self ) -> int :
277+ """Get DND enabled state."""
278+ return self .status .dnd_enabled or 0
279+
280+ @dnd_enabled .setter
281+ def dnd_enabled (self , value : int ) -> None :
282+ """Set DND enabled state."""
283+ self .status .dnd_enabled = value
284+
285+ @property
286+ def mop_mode (self ) -> int :
287+ """Get mop route mode."""
288+ return self .status .mop_mode or 0
289+
290+ @mop_mode .setter
291+ def mop_mode (self , value : int ) -> None :
292+ """Set mop route mode."""
293+ self .status .mop_mode = value
294+
295+ @property
296+ def water_box_mode (self ) -> int :
297+ """Get water box mode."""
298+ return self .status .water_box_mode or 0
299+
300+ @water_box_mode .setter
301+ def water_box_mode (self , value : int ) -> None :
302+ """Set water box mode."""
303+ self .status .water_box_mode = value
304+
305+ @property
306+ def dss (self ) -> int :
307+ """Get dock sensor status."""
308+ return self .status .dss or 0
309+
310+ @dss .setter
311+ def dss (self , value : int ) -> None :
312+ """Set dock sensor status."""
313+ self .status .dss = value
314+
315+ @property
316+ def dock_type (self ) -> RoborockDockTypeCode :
317+ """Get dock type."""
318+ return self .status .dock_type or RoborockDockTypeCode .no_dock
319+
320+ @dock_type .setter
321+ def dock_type (self , value : RoborockDockTypeCode | int ) -> None :
322+ """Set dock type."""
323+ self .status .dock_type = RoborockDockTypeCode (value )
324+
177325 @property
178326 def in_cleaning (self ) -> RoborockInCleaning :
179327 """Return global_clean_not_complete if cleaning, else complete."""
180328 return (
181329 RoborockInCleaning .global_clean_not_complete
182- if self .state == RoborockStateCode .cleaning
330+ if self .status . state == RoborockStateCode .cleaning
183331 else RoborockInCleaning .complete
184332 )
185333
186334 @property
187335 def in_returning (self ) -> int :
188336 """Return 1 if returning, else 0."""
189- return 1 if self .state == RoborockStateCode .returning_home else 0
337+ return 1 if self .status . state == RoborockStateCode .returning_home else 0
190338
191339 @property
192340 def charge_status (self ) -> RoborockChargeStatus :
193341 """Return charging if charging, else charge_waiting."""
194342 return (
195343 RoborockChargeStatus .charging
196- if self .state == RoborockStateCode .charging
344+ if self .status . state == RoborockStateCode .charging
197345 else RoborockChargeStatus .charge_waiting
198346 )
199347
200348 def get_status_dict (self ) -> dict [str , Any ]:
201349 """Generate status dict using the current simulated state."""
202- status = StatusV2 (
203- msg_ver = 2 ,
204- msg_seq = 458 ,
205- state = RoborockStateCode (self .state ),
206- battery = self .battery ,
207- clean_time = 1176 ,
208- clean_area = 20965000 ,
209- error_code = RoborockErrorCode (0 ),
210- map_present = 1 ,
211- in_cleaning = self .in_cleaning ,
212- in_returning = self .in_returning ,
213- in_fresh_state = 1 ,
214- lab_status = 1 ,
215- water_box_status = 1 ,
216- back_type = - 1 ,
217- wash_phase = 0 ,
218- wash_ready = 0 ,
219- fan_power = self .fan_power ,
220- dnd_enabled = self .dnd_enabled ,
221- map_status = 3 ,
222- is_locating = 0 ,
223- lock_status = 0 ,
224- water_box_mode = self .water_box_mode ,
225- water_box_carriage_status = 1 ,
226- mop_forbidden_enable = 1 ,
227- camera_status = 3457 ,
228- is_exploring = 0 ,
229- home_sec_status = 0 ,
230- home_sec_enable_password = 0 ,
231- adbumper_status = [0 , 0 , 0 ],
232- water_shortage_status = 0 ,
233- dock_type = RoborockDockTypeCode (self .dock_type ),
234- dust_collection_status = 0 ,
235- auto_dust_collection = 1 ,
236- avoid_count = 19 ,
237- mop_mode = self .mop_mode ,
238- debug_mode = 0 ,
239- collision_avoid_status = 1 ,
240- switch_map_mode = 0 ,
241- dock_error_status = RoborockDockErrorCode (0 ),
242- charge_status = self .charge_status ,
243- unsave_map_reason = 0 ,
244- unsave_map_flag = 0 ,
245- dss = self .dss ,
246- )
247- return _serialize_dataclass (status )
350+ self .status .in_cleaning = self .in_cleaning
351+ self .status .in_returning = self .in_returning
352+ self .status .charge_status = self .charge_status
353+ return _serialize_dataclass (self .status )
248354
249355 def _handle_app_start (self , params : Any ) -> str :
250- self .state = RoborockStateCode .cleaning
356+ self .status . state = RoborockStateCode .cleaning
251357 return "ok"
252358
253359 def _handle_app_stop (self , params : Any ) -> str :
254- self .state = RoborockStateCode .paused
360+ self .status . state = RoborockStateCode .paused
255361 return "ok"
256362
257363 def _handle_app_charge (self , params : Any ) -> str :
258- self .state = RoborockStateCode .returning_home
364+ self .status . state = RoborockStateCode .returning_home
259365 return "ok"
260366
261367 def _handle_set_custom_mode (self , params : Any ) -> str :
262368 if isinstance (params , list ) and len (params ) > 0 :
263- self .fan_power = params [0 ]
369+ self .status . fan_power = params [0 ]
264370 elif isinstance (params , dict ):
265- self .fan_power = params .get ("fan_power" , self .fan_power )
371+ self .status . fan_power = params .get ("fan_power" , self . status .fan_power )
266372 return "ok"
267373
268374 def _handle_set_mop_mode (self , params : Any ) -> str :
269375 if isinstance (params , list ) and len (params ) > 0 :
270- self .mop_mode = params [0 ]
376+ self .status . mop_mode = params [0 ]
271377 return "ok"
272378
273379 def _handle_set_water_box_custom_mode (self , params : Any ) -> str :
274380 if isinstance (params , list ) and len (params ) > 0 :
275- self .water_box_mode = params [0 ]
381+ self .status . water_box_mode = params [0 ]
276382 return "ok"
277383
278384 def _handle_reset_consumable (self , params : Any ) -> str :
@@ -305,8 +411,8 @@ def _handle_app_get_init_status(self, params: Any) -> list[dict[str, Any]]:
305411 payload ["new_feature_info2" ] = payload .pop ("new_feature_info_2" )
306412
307413 payload ["status_info" ] = {
308- "state" : self .state ,
309- "battery" : self .battery ,
414+ "state" : self .status . state . value if self . status . state else 0 ,
415+ "battery" : self .status . battery ,
310416 "clean_time" : 5610 ,
311417 "clean_area" : 96490000 ,
312418 "error_code" : 0 ,
@@ -318,7 +424,7 @@ def _handle_app_get_init_status(self, params: Any) -> list[dict[str, Any]]:
318424 "map_status" : 3 ,
319425 "is_locating" : 0 ,
320426 "lock_status" : 0 ,
321- "water_box_mode" : self .water_box_mode ,
427+ "water_box_mode" : self .status . water_box_mode ,
322428 "distance_off" : 0 ,
323429 "water_box_carriage_status" : 0 ,
324430 "mop_forbidden_enable" : 0 ,
0 commit comments