11"""Global fixtures for Roborock integration."""
22
33import asyncio
4- from collections .abc import Awaitable , Callable , Generator
4+ from collections .abc import Generator
55from copy import deepcopy
66import logging
77import pathlib
1313from roborock import RoborockCategory
1414from roborock .data import (
1515 CombinedMapInfo ,
16+ DnDTimer ,
1617 DyadError ,
1718 HomeDataDevice ,
1819 HomeDataProduct ,
1920 NamedRoomMapping ,
2021 NetworkInfo ,
22+ RoborockBase ,
2123 RoborockDyadStateCode ,
2224 ZeoError ,
2325 ZeoState ,
2426)
2527from roborock .devices .device import RoborockDevice
26- from roborock .devices .traits .v1 .map_content import MapContent
27- from roborock .devices .traits .v1 .volume import SoundVolume
28+ from roborock .devices .traits .v1 import PropertiesApi
29+ from roborock .devices .traits .v1 .clean_summary import CleanSummaryTrait
30+ from roborock .devices .traits .v1 .command import CommandTrait
31+ from roborock .devices .traits .v1 .common import V1TraitMixin
32+ from roborock .devices .traits .v1 .consumeable import ConsumableTrait
33+ from roborock .devices .traits .v1 .do_not_disturb import DoNotDisturbTrait
34+ from roborock .devices .traits .v1 .dust_collection_mode import DustCollectionModeTrait
35+ from roborock .devices .traits .v1 .home import HomeTrait
36+ from roborock .devices .traits .v1 .map_content import MapContent , MapContentTrait
37+ from roborock .devices .traits .v1 .maps import MapsTrait
38+ from roborock .devices .traits .v1 .network_info import NetworkInfoTrait
39+ from roborock .devices .traits .v1 .routines import RoutinesTrait
40+ from roborock .devices .traits .v1 .smart_wash_params import SmartWashParamsTrait
41+ from roborock .devices .traits .v1 .status import StatusTrait
42+ from roborock .devices .traits .v1 .volume import SoundVolumeTrait
43+ from roborock .devices .traits .v1 .wash_towel_mode import WashTowelModeTrait
2844from roborock .roborock_message import RoborockDyadDataProtocol , RoborockZeoProtocol
2945
3046from homeassistant .components .roborock .const import (
@@ -130,71 +146,100 @@ async def get_devices(self) -> list[RoborockDevice]:
130146 return self ._devices
131147
132148
133- def make_fake_switch (obj : Any ) -> Any :
134- """Update the fake object to emulate the switch trait behavior."""
135- obj .is_on = True
136- obj .enable = AsyncMock ()
137- obj .enable .side_effect = lambda : setattr (obj , "is_on" , True )
138- obj .disable = AsyncMock ()
139- obj .disable .side_effect = lambda : setattr (obj , "is_on" , False )
140- obj .refresh = AsyncMock ()
141- return obj
149+ def make_mock_trait (
150+ trait_spec : type [V1TraitMixin ] | None = None ,
151+ dataclass_template : RoborockBase | None = None ,
152+ ) -> AsyncMock :
153+ """Create a mock roborock trait."""
154+ trait = AsyncMock (spec = trait_spec or V1TraitMixin )
155+ if dataclass_template is not None :
156+ # Copy all attributes and property methods (e.g. computed properties)
157+ template_copy = deepcopy (dataclass_template )
158+ for attr_name in dir (template_copy ):
159+ if attr_name .startswith ("_" ):
160+ continue
161+ setattr (trait , attr_name , getattr (template_copy , attr_name ))
162+ trait .refresh = AsyncMock ()
163+ return trait
164+
165+
166+ def make_mock_switch (
167+ trait_spec : type [V1TraitMixin ] | None = None ,
168+ dataclass_template : RoborockBase | None = None ,
169+ ) -> AsyncMock :
170+ """Create a mock roborock switch trait."""
171+ trait = make_mock_trait (
172+ trait_spec = trait_spec ,
173+ dataclass_template = dataclass_template ,
174+ )
175+ trait .is_on = True
176+ trait .enable = AsyncMock ()
177+ trait .enable .side_effect = lambda : setattr (trait , "is_on" , True )
178+ trait .disable = AsyncMock ()
179+ trait .disable .side_effect = lambda : setattr (trait , "is_on" , False )
180+ return trait
142181
143182
144- def set_timer_fn ( obj : Any ) -> Callable [[ Any ], Awaitable [ None ]] :
183+ def make_dnd_timer ( dataclass_template : RoborockBase ) -> AsyncMock :
145184 """Make a function for the fake timer trait that emulates the real behavior."""
185+ dnd_trait = make_mock_switch (
186+ trait_spec = DoNotDisturbTrait ,
187+ dataclass_template = dataclass_template ,
188+ )
146189
147- async def update_timer_attributes (timer : Any ) -> None :
148- setattr (obj , "start_hour" , timer .start_hour )
149- setattr (obj , "start_minute" , timer .start_minute )
150- setattr (obj , "end_hour" , timer .end_hour )
151- setattr (obj , "end_minute" , timer .end_minute )
152- setattr (obj , "enabled" , timer .enabled )
190+ async def set_dnd_timer (timer : DnDTimer ) -> None :
191+ setattr (dnd_trait , "start_hour" , timer .start_hour )
192+ setattr (dnd_trait , "start_minute" , timer .start_minute )
193+ setattr (dnd_trait , "end_hour" , timer .end_hour )
194+ setattr (dnd_trait , "end_minute" , timer .end_minute )
195+ setattr (dnd_trait , "enabled" , timer .enabled )
153196
154- return update_timer_attributes
197+ dnd_trait .set_dnd_timer = AsyncMock ()
198+ dnd_trait .set_dnd_timer .side_effect = set_dnd_timer
199+ return dnd_trait
155200
156201
157- def create_v1_properties (network_info : NetworkInfo ) -> Mock :
202+ def create_v1_properties (network_info : NetworkInfo ) -> AsyncMock :
158203 """Create v1 properties for each fake device."""
159- v1_properties = Mock ()
160- v1_properties .status : Any = deepcopy (STATUS )
161- v1_properties .status .refresh = AsyncMock ()
162- v1_properties .dnd : Any = make_fake_switch (deepcopy (DND_TIMER ))
163- v1_properties .dnd .set_dnd_timer = AsyncMock ()
164- v1_properties .dnd .set_dnd_timer .side_effect = set_timer_fn (v1_properties .dnd )
165- v1_properties .clean_summary : Any = deepcopy (CLEAN_SUMMARY )
204+ v1_properties = AsyncMock (spec = PropertiesApi )
205+ v1_properties .status = make_mock_trait (
206+ trait_spec = StatusTrait ,
207+ dataclass_template = STATUS ,
208+ )
209+ v1_properties .dnd = make_dnd_timer (dataclass_template = DND_TIMER )
210+ v1_properties .clean_summary = make_mock_trait (
211+ trait_spec = CleanSummaryTrait ,
212+ dataclass_template = CLEAN_SUMMARY ,
213+ )
166214 v1_properties .clean_summary .last_clean_record = deepcopy (CLEAN_RECORD )
167- v1_properties .clean_summary . refresh = AsyncMock ()
168- v1_properties . consumables = deepcopy ( CONSUMABLE )
169- v1_properties . consumables . refresh = AsyncMock ( )
215+ v1_properties .consumables = make_mock_trait (
216+ trait_spec = ConsumableTrait , dataclass_template = CONSUMABLE
217+ )
170218 v1_properties .consumables .reset_consumable = AsyncMock ()
171- v1_properties .sound_volume = SoundVolume (volume = 50 )
219+ v1_properties .sound_volume = make_mock_trait (trait_spec = SoundVolumeTrait )
220+ v1_properties .sound_volume .volume = 50
172221 v1_properties .sound_volume .set_volume = AsyncMock ()
173222 v1_properties .sound_volume .set_volume .side_effect = lambda vol : setattr (
174223 v1_properties .sound_volume , "volume" , vol
175224 )
176- v1_properties .sound_volume .refresh = AsyncMock ()
177- v1_properties .command = AsyncMock ()
225+ v1_properties .command = AsyncMock (spec = CommandTrait )
178226 v1_properties .command .send = AsyncMock ()
179- v1_properties .maps = AsyncMock ( )
227+ v1_properties .maps = make_mock_trait ( trait_spec = MapsTrait )
180228 v1_properties .maps .current_map = MULTI_MAP_LIST .map_info [1 ].map_flag
181- v1_properties .maps .refresh = AsyncMock ()
182229 v1_properties .maps .set_current_map = AsyncMock ()
183- v1_properties .map_content = AsyncMock ( )
230+ v1_properties .map_content = make_mock_trait ( trait_spec = MapContentTrait )
184231 v1_properties .map_content .image_content = b"\x89 PNG-001"
185232 v1_properties .map_content .map_data = deepcopy (MAP_DATA )
186- v1_properties .map_content .refresh = AsyncMock ()
187- v1_properties .child_lock = make_fake_switch (AsyncMock ())
188- v1_properties .led_status = make_fake_switch (AsyncMock ())
189- v1_properties .flow_led_status = make_fake_switch (AsyncMock ())
190- v1_properties .valley_electricity_timer = make_fake_switch (AsyncMock ())
191- v1_properties .dust_collection_mode = AsyncMock ()
192- v1_properties .dust_collection_mode .refresh = AsyncMock ()
193- v1_properties .wash_towel_mode = AsyncMock ()
194- v1_properties .wash_towel_mode .refresh = AsyncMock ()
195- v1_properties .smart_wash_params = AsyncMock ()
196- v1_properties .smart_wash_params .refresh = AsyncMock ()
197- v1_properties .home = AsyncMock ()
233+ v1_properties .child_lock = make_mock_switch ()
234+ v1_properties .led_status = make_mock_switch ()
235+ v1_properties .flow_led_status = make_mock_switch ()
236+ v1_properties .valley_electricity_timer = make_mock_switch ()
237+ v1_properties .dust_collection_mode = make_mock_trait (
238+ trait_spec = DustCollectionModeTrait
239+ )
240+ v1_properties .wash_towel_mode = make_mock_trait (trait_spec = WashTowelModeTrait )
241+ v1_properties .smart_wash_params = make_mock_trait (trait_spec = SmartWashParamsTrait )
242+ v1_properties .home = make_mock_trait (trait_spec = HomeTrait )
198243 home_map_info = {
199244 map_data .map_flag : CombinedMapInfo (
200245 name = map_data .name ,
@@ -219,10 +264,11 @@ def create_v1_properties(network_info: NetworkInfo) -> Mock:
219264 v1_properties .home .home_map_info = home_map_info
220265 v1_properties .home .current_map_data = home_map_info [STATUS .current_map ]
221266 v1_properties .home .home_map_content = home_map_content
222- v1_properties .home .refresh = AsyncMock ()
223- v1_properties .network_info = deepcopy (network_info )
224- v1_properties .network_info .refresh = AsyncMock ()
225- v1_properties .routines = AsyncMock ()
267+ v1_properties .network_info = make_mock_trait (
268+ trait_spec = NetworkInfoTrait ,
269+ dataclass_template = network_info ,
270+ )
271+ v1_properties .routines = make_mock_trait (trait_spec = RoutinesTrait )
226272 v1_properties .routines .get_routines = AsyncMock (return_value = SCENES )
227273 v1_properties .routines .execute_routine = AsyncMock ()
228274 # Mock diagnostics for a subset of properties
@@ -267,21 +313,22 @@ def fake_vacuum_fixture(fake_devices: list[FakeDevice]) -> FakeDevice:
267313 return fake_devices [0 ]
268314
269315
270- @pytest .fixture (name = "send_message_side_effect " )
271- def send_message_side_effect_fixture () -> Any :
316+ @pytest .fixture (name = "send_message_exception " )
317+ def send_message_exception_fixture () -> Exception | None :
272318 """Fixture to return a side effect for the send_message method."""
273319 return None
274320
275321
276322@pytest .fixture (name = "vacuum_command" , autouse = True )
277323def fake_vacuum_command_fixture (
278- fake_vacuum : FakeDevice , send_message_side_effect : Any
279- ) -> Mock :
324+ fake_vacuum : FakeDevice ,
325+ send_message_exception : Exception | None ,
326+ ) -> AsyncMock :
280327 """Get the fake vacuum device command trait for asserting that commands happened."""
281328 assert fake_vacuum .v1_properties is not None
282329 command_trait = fake_vacuum .v1_properties .command
283- if send_message_side_effect is not None :
284- command_trait .send .side_effect = send_message_side_effect
330+ if send_message_exception is not None :
331+ command_trait .send .side_effect = send_message_exception
285332 return command_trait
286333
287334
0 commit comments