Repository navigation
Expand file tree
/
Copy pathtest_home.py
More file actions
346 lines (285 loc) · 11.4 KB
/
Copy pathtest_home.py
File metadata and controls
346 lines (285 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
"""Tests for the Home related functionality."""
from collections.abc import Iterator
from unittest.mock import AsyncMock, patch
import pytest
from roborock.data.containers import CombinedMapInfo
from roborock.data.v1.v1_code_mappings import RoborockStateCode
from roborock.devices.cache import InMemoryCache
from roborock.devices.device import RoborockDevice
from roborock.devices.traits.v1.home import HomeTrait
from roborock.devices.traits.v1.maps import MapsTrait
from roborock.devices.traits.v1.rooms import RoomsTrait
from roborock.devices.traits.v1.status import StatusTrait
from roborock.exceptions import RoborockDeviceBusy
from roborock.roborock_typing import RoborockCommand
from tests import mock_data
MULTI_MAP_LIST_DATA = [
{
"max_multi_map": 2,
"max_bak_map": 1,
"multi_map_count": 2,
"map_info": [
{
"mapFlag": 0,
"add_time": 1747132930,
"length": 0,
"name": "Ground Floor",
"bak_maps": [{"mapFlag": 4, "add_time": 1747132936}],
},
{
"mapFlag": 123,
"add_time": 1747132940,
"length": 0,
"name": "Second Floor",
"bak_maps": [{"mapFlag": 5, "add_time": 1747132946}],
},
],
}
]
ROOM_MAPPING_DATA_MAP_0 = [[16, "2362048"], [17, "2362044"]]
ROOM_MAPPING_DATA_MAP_123 = [[18, "2362041"], [19, "2362042"]]
UPDATED_STATUS_MAP_0 = {
**mock_data.STATUS,
"map_status": 0 * 4 + 3, # Set current map to 0
}
UPDATED_STATUS_MAP_123 = {
**mock_data.STATUS,
"map_status": 123 * 4 + 3, # Set current map to 123
}
@pytest.fixture(autouse=True)
def no_sleep() -> Iterator[None]:
"""Patch sleep to avoid delays in tests."""
with patch("roborock.devices.traits.v1.home.asyncio.sleep"):
yield
@pytest.fixture
def cache():
"""Create an in-memory cache for testing."""
return InMemoryCache()
@pytest.fixture(autouse=True)
async def status_trait(mock_rpc_channel: AsyncMock, device: RoborockDevice) -> StatusTrait:
"""Create a StatusTrait instance with mocked dependencies."""
assert device.v1_properties
status_trait = device.v1_properties.status
# Verify initial state
assert status_trait.current_map is None
mock_rpc_channel.send_command.side_effect = [UPDATED_STATUS_MAP_0]
await status_trait.refresh()
assert status_trait.current_map == 0
mock_rpc_channel.reset_mock()
return status_trait
@pytest.fixture
def maps_trait(device: RoborockDevice) -> MapsTrait:
"""Create a MapsTrait instance with mocked dependencies."""
assert device.v1_properties
return device.v1_properties.maps
@pytest.fixture
def rooms_trait(device: RoborockDevice) -> RoomsTrait:
"""Create a RoomsTrait instance with mocked dependencies."""
assert device.v1_properties
return device.v1_properties.rooms
@pytest.fixture
def home_trait(status_trait: StatusTrait, maps_trait: MapsTrait, rooms_trait: RoomsTrait, cache) -> HomeTrait:
"""Create a HomeTrait instance with mocked dependencies."""
return HomeTrait(status_trait, maps_trait, rooms_trait, cache)
async def test_discover_home_empty_cache(
status_trait: StatusTrait,
home_trait: HomeTrait,
mock_rpc_channel: AsyncMock,
mock_mqtt_rpc_channel: AsyncMock,
) -> None:
"""Test discovering home when cache is empty."""
# Setup mocks for the discovery process
mock_rpc_channel.send_command.side_effect = [
UPDATED_STATUS_MAP_123, # Status after switching to map 123
ROOM_MAPPING_DATA_MAP_123, # Rooms for map 123
UPDATED_STATUS_MAP_0, # Status after switching back to map 0
ROOM_MAPPING_DATA_MAP_0, # Rooms for map 0
]
mock_mqtt_rpc_channel.send_command.side_effect = [
MULTI_MAP_LIST_DATA, # Multi maps list
{}, # LOAD_MULTI_MAP response for map 123
{}, # LOAD_MULTI_MAP response back to map 0
]
# Before discovery, no cache should exist
assert home_trait.home_map_info is None
assert home_trait.current_map_data is None
# Perform home discovery
await home_trait.discover_home()
# Verify cache is populated
assert home_trait.home_map_info is not None
assert len(home_trait.home_map_info) == 2
# Check map 0 data
map_0_data = home_trait.home_map_info[0]
assert map_0_data.map_flag == 0
assert map_0_data.name == "Ground Floor"
assert len(map_0_data.rooms) == 2
assert map_0_data.rooms[0].segment_id == 16
assert map_0_data.rooms[0].name == "Example room 1"
assert map_0_data.rooms[1].segment_id == 17
assert map_0_data.rooms[1].name == "Example room 2"
# Check map 123 data
map_123_data = home_trait.home_map_info[123]
assert map_123_data.map_flag == 123
assert map_123_data.name == "Second Floor"
assert len(map_123_data.rooms) == 2
assert map_123_data.rooms[0].segment_id == 18
assert map_123_data.rooms[0].name == "Example room 3"
assert map_123_data.rooms[1].segment_id == 19
assert map_123_data.rooms[1].name == "Unknown" # Not in mock home data
# Verify current map data is accessible
current_map_data = home_trait.current_map_data
assert current_map_data is not None
assert current_map_data.map_flag == 0
assert current_map_data.name == "Ground Floor"
async def test_discover_home_with_existing_cache(
home_trait: HomeTrait,
mock_rpc_channel: AsyncMock,
mock_mqtt_rpc_channel: AsyncMock,
) -> None:
"""Test that discovery is skipped when cache already exists."""
# Pre-populate the cache
cache_data = await home_trait._cache.get()
cache_data.home_map_info = {0: CombinedMapInfo(map_flag=0, name="Dummy", rooms=[])}
await home_trait._cache.set(cache_data)
# Call discover_home
await home_trait.discover_home()
# Verify no RPC calls were made (discovery was skipped)
assert mock_rpc_channel.send_command.call_count == 0
assert mock_mqtt_rpc_channel.send_command.call_count == 0
# Verify cache was loaded from storage
assert home_trait.home_map_info == {0: CombinedMapInfo(map_flag=0, name="Dummy", rooms=[])}
async def test_discover_home_no_maps(
home_trait: HomeTrait,
mock_rpc_channel: AsyncMock,
mock_mqtt_rpc_channel: AsyncMock,
) -> None:
"""Test discovery when no maps are available."""
# Setup mock to return empty maps list
mock_mqtt_rpc_channel.send_command.side_effect = [
[{"max_multi_map": 0, "max_bak_map": 0, "multi_map_count": 0, "map_info": []}]
]
with pytest.raises(Exception, match="Cannot perform home discovery without current map info"):
await home_trait.discover_home()
async def test_refresh_updates_current_map_cache(
status_trait: StatusTrait,
home_trait: HomeTrait,
mock_rpc_channel: AsyncMock,
mock_mqtt_rpc_channel: AsyncMock,
) -> None:
"""Test that refresh updates the cache for the current map."""
# Pre-populate cache with some data
cache_data = await home_trait._cache.get()
cache_data.home_map_info = {0: CombinedMapInfo(map_flag=0, name="Old Ground Floor", rooms=[])}
await home_trait._cache.set(cache_data)
home_trait._home_map_info = cache_data.home_map_info
# Setup mocks for refresh
mock_rpc_channel.send_command.side_effect = [
ROOM_MAPPING_DATA_MAP_0, # Room mapping refresh
]
mock_mqtt_rpc_channel.send_command.side_effect = [
MULTI_MAP_LIST_DATA, # Maps refresh
]
# Perform refresh
await home_trait.refresh()
# Verify cache was updated for current map
updated_cache = home_trait.home_map_info
assert updated_cache is not None
assert 0 in updated_cache
map_data = updated_cache[0]
assert map_data.name == "Ground Floor" # Updated from "Old Ground Floor"
assert len(map_data.rooms) == 2
async def test_refresh_no_cache_no_update(
home_trait: HomeTrait,
mock_rpc_channel: AsyncMock,
mock_mqtt_rpc_channel: AsyncMock,
) -> None:
"""Test that refresh doesn't update when no cache exists."""
# Setup mocks for refresh
# mock_mqtt_rpc_channel.send_command.side_effect = [
# MULTI_MAP_LIST_DATA, # Maps refresh
# ]
# Perform refresh without existing cache
with pytest.raises(Exception, match="Cannot refresh home data without home cache, did you call discover_home()?"):
await home_trait.refresh()
async def test_current_map_data_property(
home_trait: HomeTrait,
mock_rpc_channel: AsyncMock,
mock_mqtt_rpc_channel: AsyncMock,
) -> None:
"""Test current_map_data property returns correct data."""
# Setup discovery
mock_rpc_channel.send_command.side_effect = [
UPDATED_STATUS_MAP_123, # Status after switching to map 123
ROOM_MAPPING_DATA_MAP_123, # Rooms for map 123
UPDATED_STATUS_MAP_0, # Status after switching back to map 0
ROOM_MAPPING_DATA_MAP_0, # Rooms for map 0
]
mock_mqtt_rpc_channel.send_command.side_effect = [
MULTI_MAP_LIST_DATA, # Multi maps list
{}, # LOAD_MULTI_MAP response for map 123
{}, # LOAD_MULTI_MAP response back to map 0
]
await home_trait.discover_home()
# Test current map data (should be map 0)
current_data = home_trait.current_map_data
assert current_data is not None
assert current_data.map_flag == 0
assert current_data.name == "Ground Floor"
# Test when no cache exists
home_trait._home_map_info = None
assert home_trait.current_map_data is None
async def test_discover_home_device_busy_cleaning(
status_trait: StatusTrait,
home_trait: HomeTrait,
mock_rpc_channel: AsyncMock,
mock_mqtt_rpc_channel: AsyncMock,
) -> None:
"""Test that discovery raises RoborockDeviceBusy when device is cleaning."""
# Set the status trait state to cleaning
status_trait.state = RoborockStateCode.cleaning
# Attempt to discover home while cleaning
with pytest.raises(RoborockDeviceBusy, match="Cannot perform home discovery while the device is cleaning"):
await home_trait.discover_home()
# Verify no RPC calls were made (discovery was prevented)
assert mock_rpc_channel.send_command.call_count == 0
assert mock_mqtt_rpc_channel.send_command.call_count == 0
async def test_single_map_no_switching(
home_trait: HomeTrait,
mock_rpc_channel: AsyncMock,
mock_mqtt_rpc_channel: AsyncMock,
) -> None:
"""Test that single map discovery doesn't trigger map switching."""
single_map_data = [
{
"max_multi_map": 1,
"max_bak_map": 0,
"multi_map_count": 1,
"map_info": [
{
"mapFlag": 0,
"add_time": 1747132930,
"length": 0,
"name": "Only Floor",
"bak_maps": [],
},
],
}
]
mock_rpc_channel.send_command.side_effect = [
ROOM_MAPPING_DATA_MAP_0, # Rooms for the single map
]
mock_mqtt_rpc_channel.send_command.side_effect = [
single_map_data, # Single map list
]
await home_trait.discover_home()
# Verify cache is populated
assert home_trait.home_map_info is not None
assert len(home_trait.home_map_info) == 1
assert 0 in home_trait.home_map_info
# Verify no LOAD_MULTI_MAP commands were sent (no map switching)
load_map_calls = [
call
for call in mock_mqtt_rpc_channel.send_command.call_args_list
if call[1].get("command") == RoborockCommand.LOAD_MULTI_MAP
]
assert len(load_map_calls) == 0