Skip to content

Commit 12b510d

Browse files
committed
fix: Fix DeviceFeatures so that it can be serialized and deserialized properly.
Adds tests for the desired output.
1 parent b48121f commit 12b510d

File tree

5 files changed

+76
-8
lines changed

5 files changed

+76
-8
lines changed

SUPPORTED_FEATURES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
| `is_carpet_shape_type_supported` | | | |
3939
| `is_carpet_show_on_map` | | X | |
4040
| `is_carpet_supported` | X | X | |
41-
| `is_ces2022_supported` | | | |
41+
| `is_ces_2022_supported` | | | |
4242
| `is_clean_count_setting_supported` | | X | |
4343
| `is_clean_direct_status_supported` | | | |
4444
| `is_clean_efficiency_supported` | | | |

roborock/data/containers.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import dataclasses
22
import datetime
3+
import inspect
34
import json
45
import logging
56
import re
@@ -27,7 +28,11 @@ def _camelize(s: str):
2728

2829

2930
def _decamelize(s: str):
30-
return re.sub("([A-Z]+)", "_\\1", s).lower()
31+
# Split before uppercase letters not at the start, and before numbers
32+
s = re.sub(r"(?<=[a-z0-9])([A-Z])", r"_\1", s)
33+
s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s) # Split acronyms followed by normal camelCase
34+
s = re.sub(r"([a-zA-Z])([0-9]+)", r"\1_\2", s)
35+
return s.lower()
3136

3237

3338
def _attr_repr(obj: Any) -> str:
@@ -64,11 +69,12 @@ def _convert_to_class_obj(class_type: type, value):
6469
if get_origin(class_type) is dict:
6570
_, value_type = get_args(class_type) # assume keys are only basic types
6671
return {k: RoborockBase._convert_to_class_obj(value_type, v) for k, v in value.items()}
67-
if issubclass(class_type, RoborockBase):
68-
return class_type.from_dict(value)
69-
if issubclass(class_type, RoborockModeEnum):
70-
return class_type.from_code(value)
71-
if class_type is Any:
72+
if inspect.isclass(class_type):
73+
if issubclass(class_type, RoborockBase):
74+
return class_type.from_dict(value)
75+
if issubclass(class_type, RoborockModeEnum):
76+
return class_type.from_code(value)
77+
if class_type is Any or type(class_type) is str:
7278
return value
7379
return class_type(value) # type: ignore[call-arg]
7480

roborock/device_features.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ class DeviceFeatures(RoborockBase):
313313
is_support_incremental_map: bool = field(metadata={"new_feature_str_mask": (4194304, 8)})
314314
is_offline_map_supported: bool = field(metadata={"new_feature_str_mask": (16384, 8)})
315315
is_super_deep_wash_supported: bool = field(metadata={"new_feature_str_mask": (32768, 8)})
316-
is_ces2022_supported: bool = field(metadata={"new_feature_str_mask": (65536, 8)})
316+
is_ces_2022_supported: bool = field(metadata={"new_feature_str_mask": (65536, 8)})
317317
is_dss_believable: bool = field(metadata={"new_feature_str_mask": (131072, 8)})
318318
is_main_brush_up_down_supported_from_str: bool = field(metadata={"new_feature_str_mask": (262144, 8)})
319319
is_goto_pure_clean_path_supported: bool = field(metadata={"new_feature_str_mask": (524288, 8)})

tests/test_containers.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""Test cases for the containers module."""
22

3+
import dataclasses
34
from dataclasses import dataclass
45
from typing import Any
56

7+
import pytest
68
from syrupy import SnapshotAssertion
79

810
from roborock import CleanRecord, CleanSummary, Consumable, DnDTimer, HomeData, S7MaxVStatus, UserData
@@ -13,6 +15,7 @@
1315
SCWindMapping,
1416
WorkStatusMapping,
1517
)
18+
from roborock.data.containers import _camelize, _decamelize
1619
from roborock.data.v1 import (
1720
MultiMapsList,
1821
RoborockDockErrorCode,
@@ -58,6 +61,15 @@ class ComplexObject(RoborockBase):
5861
any: Any | None = None
5962

6063

64+
@dataclass
65+
class BoolFeatures(RoborockBase):
66+
"""Complex object for testing serialization."""
67+
68+
my_flag_supported: bool | None = None
69+
my_flag_2_supported: bool | None = None
70+
is_ces_2022_supported: bool | None = None
71+
72+
6173
def test_simple_object() -> None:
6274
"""Test serialization and deserialization of a simple object."""
6375

@@ -494,3 +506,37 @@ def test_accurate_map_flag() -> None:
494506
}
495507
)
496508
assert s.current_map is None
509+
510+
511+
def test_boolean_features() -> None:
512+
"""Test serialization and deserialization of BoolFeatures."""
513+
obj = BoolFeatures(my_flag_supported=True, my_flag_2_supported=False, is_ces_2022_supported=True)
514+
serialized = obj.as_dict()
515+
assert serialized == {
516+
"myFlagSupported": True,
517+
"myFlag2Supported": False,
518+
"isCes2022Supported": True,
519+
}
520+
deserialized = BoolFeatures.from_dict(serialized)
521+
assert dataclasses.asdict(deserialized) == {
522+
"my_flag_supported": True,
523+
"my_flag_2_supported": False,
524+
"is_ces_2022_supported": True,
525+
}
526+
527+
528+
@pytest.mark.parametrize(
529+
"input_str,expected",
530+
[
531+
("simpleTest", "simple_test"),
532+
("testValue", "test_value"),
533+
("anotherExampleHere", "another_example_here"),
534+
("isCes2022Supported", "is_ces_2022_supported"),
535+
("isThreeDMappingInnerTestSupported", "is_three_d_mapping_inner_test_supported"),
536+
],
537+
)
538+
def test_decamelize_function(input_str: str, expected: str) -> None:
539+
"""Test the _decamelize function."""
540+
541+
assert _decamelize(input_str) == expected
542+
assert _camelize(expected) == input_str

tests/test_supported_features.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
from syrupy import SnapshotAssertion
2+
13
from roborock import SHORT_MODEL_TO_ENUM
4+
from roborock.data.code_mappings import RoborockProductNickname
25
from roborock.device_features import DeviceFeatures
36

47

@@ -43,3 +46,16 @@ def test_supported_features_s7():
4346
assert not device_features.is_hot_wash_towel_supported
4447
num_true = sum(vars(device_features).values())
4548
assert num_true != 0
49+
50+
51+
def test_device_feature_serialization(snapshot: SnapshotAssertion) -> None:
52+
"""Test serialization and deserialization of DeviceFeatures."""
53+
device_features = DeviceFeatures.from_feature_flags(
54+
new_feature_info=636084721975295,
55+
new_feature_info_str="0000000000002000",
56+
feature_info=[111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 122, 123, 124, 125],
57+
product_nickname=RoborockProductNickname.TANOSS,
58+
)
59+
serialized = device_features.as_dict()
60+
deserialized = DeviceFeatures.from_dict(serialized)
61+
assert deserialized == device_features

0 commit comments

Comments
 (0)