Skip to content

Commit 9ae1e66

Browse files
committed
chore: add testing
1 parent 69671bb commit 9ae1e66

File tree

6 files changed

+143
-8
lines changed

6 files changed

+143
-8
lines changed

roborock/data/code_mappings.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import logging
44
from collections import namedtuple
55
from enum import Enum, IntEnum, StrEnum
6-
from typing import Self
6+
from typing import Any, Self
77

88
_LOGGER = logging.getLogger(__name__)
99
completed_warnings = set()
@@ -104,6 +104,13 @@ def keys(cls) -> list[str]:
104104
"""Returns a list of all member values."""
105105
return [member.value for member in cls]
106106

107+
def __eq__(self, other: Any) -> bool:
108+
if isinstance(other, str):
109+
return self.value == other or self.name == other
110+
if isinstance(other, int):
111+
return self.code == other
112+
return super().__eq__(other)
113+
107114

108115
ProductInfo = namedtuple("ProductInfo", ["nickname", "short_models"])
109116

roborock/devices/traits/v1/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,14 +190,13 @@ def __init__(
190190
self._device_cache = device_cache
191191
self._region = region
192192

193-
self.device_features = DeviceFeaturesTrait(product.product_nickname, self._device_cache)
193+
self.device_features = DeviceFeaturesTrait(product, self._device_cache)
194194
self.status = StatusTrait(self.device_features, region=self._region)
195195
self.consumables = ConsumableTrait()
196196
self.rooms = RoomsTrait(home_data)
197197
self.maps = MapsTrait(self.status)
198198
self.map_content = MapContentTrait(map_parser_config)
199199
self.home = HomeTrait(self.status, self.maps, self.map_content, self.rooms, self._device_cache)
200-
self.device_features = DeviceFeaturesTrait(product, self._device_cache)
201200
self.network_info = NetworkInfoTrait(device_uid, self._device_cache)
202201
self.routines = RoutinesTrait(device_uid, web_api)
203202

roborock/devices/traits/v1/status.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,23 @@ def fan_speed_options(self) -> list[VacuumModes]:
2525

2626
@cached_property
2727
def fan_speed_mapping(self) -> dict[int, str]:
28-
return {fan.code: fan.name for fan in self.fan_speed_options}
28+
return {fan.code: fan.value for fan in self.fan_speed_options}
2929

3030
@cached_property
3131
def water_mode_options(self) -> list[WaterModes]:
3232
return get_water_modes(self._device_features_trait)
3333

3434
@cached_property
3535
def water_mode_mapping(self) -> dict[int, str]:
36-
return {mop.code: mop.name for mop in self.water_mode_options}
36+
return {mop.code: mop.value for mop in self.water_mode_options}
3737

3838
@cached_property
3939
def mop_route_options(self) -> list[CleanRoutes]:
4040
return get_clean_routes(self._device_features_trait, self._region or "us")
4141

4242
@cached_property
4343
def mop_route_mapping(self) -> dict[int, str]:
44-
return {route.code: route.name for route in self.mop_route_options}
44+
return {route.code: route.value for route in self.mop_route_options}
4545

4646
@property
4747
def fan_speed_name(self) -> str | None:

tests/data/v1/test_v1_containers.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,15 @@
1212
RoborockMopModeS7,
1313
RoborockStateCode,
1414
)
15-
from roborock.data.v1.v1_containers import AppInitStatus, CleanRecord, CleanSummary, Consumable, DnDTimer, S7MaxVStatus
15+
from roborock.data.v1.v1_containers import (
16+
AppInitStatus,
17+
CleanRecord,
18+
CleanSummary,
19+
Consumable,
20+
DnDTimer,
21+
S7MaxVStatus,
22+
StatusV2,
23+
)
1624
from tests.mock_data import (
1725
CLEAN_RECORD,
1826
CLEAN_SUMMARY,
@@ -101,6 +109,45 @@ def test_current_map() -> None:
101109
assert not s.current_map
102110

103111

112+
def test_status_v2() -> None:
113+
"""Test that StatusV2 can be created from a dictionary."""
114+
s = StatusV2.from_dict(STATUS)
115+
assert s.msg_ver == 2
116+
assert s.msg_seq == 458
117+
assert s.state == RoborockStateCode.charging
118+
assert s.battery == 100
119+
assert s.clean_time == 1176
120+
assert s.clean_area == 20965000
121+
assert s.square_meter_clean_area == 21.0
122+
assert s.error_code == RoborockErrorCode.none
123+
assert s.error_code_name == "none"
124+
assert s.state_name == "charging"
125+
assert s.map_present == 1
126+
assert s.in_cleaning == 0
127+
assert s.fan_power == 102
128+
assert s.water_box_mode == 203
129+
assert s.mop_mode == 300
130+
assert s.dock_type == RoborockDockTypeCode.empty_wash_fill_dock
131+
assert s.dock_error_status == RoborockDockErrorCode.ok
132+
assert s.current_map == 0
133+
134+
135+
def test_status_v2_current_map() -> None:
136+
"""Test the current map logic based on map status for StatusV2."""
137+
s = StatusV2.from_dict(STATUS)
138+
assert s.map_status == 3
139+
assert s.current_map == 0
140+
141+
s.map_status = 7
142+
assert s.current_map == 1
143+
144+
s.map_status = 11
145+
assert s.current_map == 2
146+
147+
s.map_status = None
148+
assert s.current_map is None
149+
150+
104151
def test_dnd_timer():
105152
dnd = DnDTimer.from_dict(DND_TIMER)
106153
assert dnd.start_hour == 22

tests/devices/__snapshots__/test_v1_device.ambr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -832,7 +832,7 @@
832832
})
833833
# ---
834834
# name: test_device_trait_command_parsing[status]
835-
StatusTrait(adbumper_status=None, auto_dust_collection=None, avoid_count=None, back_type=None, battery=100, camera_status=None, charge_status=None, clean_area=91287500, clean_fluid_status=None, clean_percent=None, clean_time=5405, clear_water_box_status=None, collision_avoid_status=None, command=<RoborockCommand.GET_STATUS: 'get_status'>, common_status=None, corner_clean_mode=None, current_map=0, debug_mode=None, dirty_water_box_status=None, distance_off=0, dnd_enabled=1, dock_cool_fan_status=None, dock_error_status=None, dock_type=None, dry_status=None, dss=None, dust_bag_status=None, dust_collection_status=None, error_code=<RoborockErrorCode.none: 0>, error_code_name='none', fan_power=106, fan_speed_mapping={101: 'QUIET', 102: 'BALANCED', 103: 'TURBO', 104: 'MAX', 105: 'GENTLE'}, fan_speed_name=None, fan_speed_options=[<VacuumModes.QUIET: 'quiet'>, <VacuumModes.BALANCED: 'balanced'>, <VacuumModes.TURBO: 'turbo'>, <VacuumModes.MAX: 'max'>, <VacuumModes.GENTLE: 'gentle'>], hatch_door_status=None, home_sec_enable_password=None, home_sec_status=None, in_cleaning=<RoborockInCleaning.complete: 0>, in_fresh_state=1, in_returning=0, in_warmup=None, is_exploring=None, is_locating=0, kct=None, lab_status=1, last_clean_t=None, lock_status=0, map_present=1, map_status=3, mop_forbidden_enable=0, mop_mode=None, mop_route_mapping={300: 'STANDARD', 301: 'DEEP'}, mop_route_name=None, mop_route_options=[<CleanRoutes.STANDARD: 'standard'>, <CleanRoutes.DEEP: 'deep'>], msg_seq=515, msg_ver=2, rdt=None, repeat=None, replenish_mode=None, rss=None, square_meter_clean_area=91.3, state=<RoborockStateCode.charging: 8>, state_name='charging', subdivision_sets=None, switch_map_mode=None, unsave_map_flag=0, unsave_map_reason=4, wash_phase=None, wash_ready=None, wash_status=None, water_box_carriage_status=0, water_box_filter_status=None, water_box_mode=204, water_box_status=0, water_mode_mapping={200: 'OFF', 201: 'LOW', 202: 'MEDIUM', 203: 'HIGH'}, water_mode_name=None, water_mode_options=[<WaterModes.OFF: 'off'>, <WaterModes.LOW: 'low'>, <WaterModes.MEDIUM: 'medium'>, <WaterModes.HIGH: 'high'>], water_shortage_status=None)
835+
StatusTrait(adbumper_status=None, auto_dust_collection=None, avoid_count=None, back_type=None, battery=100, camera_status=None, charge_status=None, clean_area=91287500, clean_fluid_status=None, clean_percent=None, clean_time=5405, clear_water_box_status=None, collision_avoid_status=None, command=<RoborockCommand.GET_STATUS: 'get_status'>, common_status=None, corner_clean_mode=None, current_map=0, debug_mode=None, dirty_water_box_status=None, distance_off=0, dnd_enabled=1, dock_cool_fan_status=None, dock_error_status=None, dock_type=None, dry_status=None, dss=None, dust_bag_status=None, dust_collection_status=None, error_code=<RoborockErrorCode.none: 0>, error_code_name='none', fan_power=106, fan_speed_mapping={101: 'quiet', 102: 'balanced', 103: 'turbo', 104: 'max', 108: 'max_plus', 105: 'off', 106: 'custom'}, fan_speed_name='custom', fan_speed_options=[<VacuumModes.QUIET: 'quiet'>, <VacuumModes.BALANCED: 'balanced'>, <VacuumModes.TURBO: 'turbo'>, <VacuumModes.MAX: 'max'>, <VacuumModes.MAX_PLUS: 'max_plus'>, <VacuumModes.OFF: 'off'>, <VacuumModes.CUSTOMIZED: 'custom'>], hatch_door_status=None, home_sec_enable_password=None, home_sec_status=None, in_cleaning=<RoborockInCleaning.complete: 0>, in_fresh_state=1, in_returning=0, in_warmup=None, is_exploring=None, is_locating=0, kct=None, lab_status=1, last_clean_t=None, lock_status=0, map_present=1, map_status=3, mop_forbidden_enable=0, mop_mode=None, mop_route_mapping={300: 'standard', 301: 'deep', 302: 'custom'}, mop_route_name=None, mop_route_options=[<CleanRoutes.STANDARD: 'standard'>, <CleanRoutes.DEEP: 'deep'>, <CleanRoutes.CUSTOMIZED: 'custom'>], msg_seq=515, msg_ver=2, rdt=None, repeat=None, replenish_mode=None, rss=None, square_meter_clean_area=91.3, state=<RoborockStateCode.charging: 8>, state_name='charging', subdivision_sets=None, switch_map_mode=None, unsave_map_flag=0, unsave_map_reason=4, wash_phase=None, wash_ready=None, wash_status=None, water_box_carriage_status=0, water_box_filter_status=None, water_box_mode=204, water_box_status=0, water_mode_mapping={200: 'off', 201: 'mild', 202: 'standard', 203: 'intense', 207: 'custom_water_flow', 204: 'custom'}, water_mode_name='custom', water_mode_options=[<WaterModes.OFF: 'off'>, <WaterModes.MILD: 'mild'>, <WaterModes.STANDARD: 'standard'>, <WaterModes.INTENSE: 'intense'>, <WaterModes.CUSTOM: 'custom_water_flow'>, <WaterModes.CUSTOMIZED: 'custom'>], water_shortage_status=None)
836836
# ---
837837
# name: test_device_trait_command_parsing[status].1
838838
dict({
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""Tests for the StatusTrait class."""
2+
3+
from unittest.mock import AsyncMock
4+
5+
import pytest
6+
7+
from roborock.data.v1 import (
8+
RoborockStateCode,
9+
)
10+
from roborock.devices.device import RoborockDevice
11+
from roborock.devices.traits.v1.status import StatusTrait
12+
from roborock.exceptions import RoborockException
13+
from roborock.roborock_typing import RoborockCommand
14+
from tests.mock_data import STATUS
15+
16+
17+
@pytest.fixture
18+
def status_trait(device: RoborockDevice) -> StatusTrait:
19+
"""Create a StatusTrait instance with mocked dependencies."""
20+
assert device.v1_properties
21+
return device.v1_properties.status
22+
23+
24+
async def test_refresh_status(status_trait: StatusTrait, mock_rpc_channel: AsyncMock) -> None:
25+
"""Test successfully refreshing status."""
26+
mock_rpc_channel.send_command.return_value = [STATUS]
27+
28+
await status_trait.refresh()
29+
30+
assert status_trait.battery == 100
31+
assert status_trait.state == RoborockStateCode.charging
32+
assert status_trait.fan_power == 102
33+
assert status_trait.fan_speed_name == "balanced"
34+
assert status_trait.fan_speed_name in status_trait.fan_speed_options
35+
mock_rpc_channel.send_command.assert_called_once_with(RoborockCommand.GET_STATUS)
36+
37+
38+
async def test_refresh_status_dict_response(status_trait: StatusTrait, mock_rpc_channel: AsyncMock) -> None:
39+
"""Test refreshing status when response is a dict instead of list."""
40+
mock_rpc_channel.send_command.return_value = STATUS
41+
42+
await status_trait.refresh()
43+
44+
assert status_trait.battery == 100
45+
assert status_trait.state == RoborockStateCode.charging
46+
mock_rpc_channel.send_command.assert_called_once_with(RoborockCommand.GET_STATUS)
47+
48+
49+
async def test_refresh_status_propagates_exception(status_trait: StatusTrait, mock_rpc_channel: AsyncMock) -> None:
50+
"""Test that exceptions from RPC channel are propagated."""
51+
mock_rpc_channel.send_command.side_effect = RoborockException("Communication error")
52+
53+
with pytest.raises(RoborockException, match="Communication error"):
54+
await status_trait.refresh()
55+
56+
57+
async def test_refresh_status_invalid_format(status_trait: StatusTrait, mock_rpc_channel: AsyncMock) -> None:
58+
"""Test that invalid response format raises ValueError."""
59+
mock_rpc_channel.send_command.return_value = "invalid"
60+
61+
with pytest.raises(ValueError, match="Unexpected status format"):
62+
await status_trait.refresh()
63+
64+
65+
def test_none_values(status_trait: StatusTrait) -> None:
66+
"""Test that none values are returned correctly."""
67+
status_trait.fan_power = None
68+
status_trait.water_box_mode = None
69+
status_trait.mop_mode = None
70+
assert status_trait.fan_speed_name is None
71+
assert status_trait.water_mode_name is None
72+
assert status_trait.mop_route_name is None
73+
74+
75+
def test_options(status_trait: StatusTrait) -> None:
76+
"""Test that fan_speed_options returns a list of options."""
77+
assert isinstance(status_trait.fan_speed_options, list)
78+
assert len(status_trait.fan_speed_options) > 0
79+
assert isinstance(status_trait.water_mode_options, list)
80+
assert len(status_trait.water_mode_options) > 0
81+
assert isinstance(status_trait.mop_route_options, list)
82+
assert len(status_trait.mop_route_options) > 0

0 commit comments

Comments
 (0)