python-roborock 4.22.0__tar.gz → 4.23.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. {python_roborock-4.22.0 → python_roborock-4.23.0}/PKG-INFO +1 -1
  2. {python_roborock-4.22.0 → python_roborock-4.23.0}/pyproject.toml +1 -1
  3. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/child_lock.py +1 -0
  4. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/clean_summary.py +37 -30
  5. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/common.py +65 -62
  6. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/consumeable.py +1 -0
  7. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/device_features.py +24 -14
  8. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/do_not_disturb.py +1 -0
  9. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/dust_collection_mode.py +1 -0
  10. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/flow_led_status.py +1 -0
  11. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/home.py +2 -6
  12. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/led_status.py +18 -14
  13. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/map_content.py +20 -11
  14. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/maps.py +19 -15
  15. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/network_info.py +12 -7
  16. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/rooms.py +56 -42
  17. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/smart_wash_params.py +1 -0
  18. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/status.py +1 -9
  19. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/valley_electricity_timer.py +1 -0
  20. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/volume.py +5 -7
  21. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/wash_towel_mode.py +1 -0
  22. {python_roborock-4.22.0 → python_roborock-4.23.0}/.gitignore +0 -0
  23. {python_roborock-4.22.0 → python_roborock-4.23.0}/LICENSE +0 -0
  24. {python_roborock-4.22.0 → python_roborock-4.23.0}/README.md +0 -0
  25. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/__init__.py +0 -0
  26. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/broadcast_protocol.py +0 -0
  27. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/callbacks.py +0 -0
  28. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/cli.py +0 -0
  29. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/const.py +0 -0
  30. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/__init__.py +0 -0
  31. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/b01_q10/__init__.py +0 -0
  32. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
  33. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
  34. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/b01_q7/__init__.py +0 -0
  35. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
  36. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
  37. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/code_mappings.py +0 -0
  38. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/containers.py +0 -0
  39. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/dyad/__init__.py +0 -0
  40. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  41. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/dyad/dyad_containers.py +0 -0
  42. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/v1/__init__.py +0 -0
  43. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/v1/v1_clean_modes.py +0 -0
  44. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/v1/v1_code_mappings.py +0 -0
  45. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/v1/v1_containers.py +0 -0
  46. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/zeo/__init__.py +0 -0
  47. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
  48. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/zeo/zeo_containers.py +0 -0
  49. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/device_features.py +0 -0
  50. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/README.md +0 -0
  51. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/__init__.py +0 -0
  52. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/cache.py +0 -0
  53. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/device.py +0 -0
  54. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/device_manager.py +0 -0
  55. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/file_cache.py +0 -0
  56. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/rpc/__init__.py +0 -0
  57. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/rpc/a01_channel.py +0 -0
  58. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/rpc/b01_q10_channel.py +0 -0
  59. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/rpc/b01_q7_channel.py +0 -0
  60. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/rpc/v1_channel.py +0 -0
  61. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/__init__.py +0 -0
  62. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/a01/__init__.py +0 -0
  63. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/__init__.py +0 -0
  64. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q10/__init__.py +0 -0
  65. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q10/command.py +0 -0
  66. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q10/common.py +0 -0
  67. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q10/status.py +0 -0
  68. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
  69. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q7/__init__.py +0 -0
  70. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q7/clean_summary.py +0 -0
  71. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q7/map.py +0 -0
  72. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/traits_mixin.py +0 -0
  73. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/__init__.py +0 -0
  74. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/command.py +0 -0
  75. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/routines.py +0 -0
  76. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/transport/__init__.py +0 -0
  77. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/transport/channel.py +0 -0
  78. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/transport/local_channel.py +0 -0
  79. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/transport/mqtt_channel.py +0 -0
  80. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/diagnostics.py +0 -0
  81. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/exceptions.py +0 -0
  82. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/map/__init__.py +0 -0
  83. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/map/map_parser.py +0 -0
  84. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/mqtt/__init__.py +0 -0
  85. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/mqtt/health_manager.py +0 -0
  86. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/mqtt/roborock_session.py +0 -0
  87. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/mqtt/session.py +0 -0
  88. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/protocol.py +0 -0
  89. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/protocols/__init__.py +0 -0
  90. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/protocols/a01_protocol.py +0 -0
  91. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/protocols/b01_q10_protocol.py +0 -0
  92. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/protocols/b01_q7_protocol.py +0 -0
  93. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/protocols/v1_protocol.py +0 -0
  94. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/py.typed +0 -0
  95. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/roborock_message.py +0 -0
  96. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/roborock_typing.py +0 -0
  97. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/util.py +0 -0
  98. {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-roborock
3
- Version: 4.22.0
3
+ Version: 4.23.0
4
4
  Summary: A package to control Roborock vacuums.
5
5
  Project-URL: Repository, https://github.com/humbertogontijo/python-roborock
6
6
  Project-URL: Documentation, https://python-roborock.readthedocs.io/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-roborock"
3
- version = "4.22.0"
3
+ version = "4.23.0"
4
4
  description = "A package to control Roborock vacuums."
5
5
  authors = [{ name = "humbertogontijo", email = "humbertogontijo@users.noreply.github.com" }, {name="Lash-L"}, {name="allenporter"}]
6
6
  requires-python = ">=3.11, <4"
@@ -9,6 +9,7 @@ class ChildLockTrait(ChildLockStatus, common.V1TraitMixin, common.RoborockSwitch
9
9
  """Trait for controlling the child lock of a Roborock device."""
10
10
 
11
11
  command = RoborockCommand.GET_CHILD_LOCK_STATUS
12
+ converter = common.DefaultConverter(ChildLockStatus)
12
13
  requires_feature = "is_set_child_supported"
13
14
 
14
15
  @property
@@ -1,7 +1,6 @@
1
1
  import logging
2
- from typing import Self
3
2
 
4
- from roborock.data import CleanRecord, CleanSummaryWithDetail
3
+ from roborock.data import CleanRecord, CleanSummaryWithDetail, RoborockBase
5
4
  from roborock.devices.traits.v1 import common
6
5
  from roborock.roborock_typing import RoborockCommand
7
6
  from roborock.util import unpack_list
@@ -9,48 +8,30 @@ from roborock.util import unpack_list
9
8
  _LOGGER = logging.getLogger(__name__)
10
9
 
11
10
 
12
- class CleanSummaryTrait(CleanSummaryWithDetail, common.V1TraitMixin):
13
- """Trait for managing the clean summary of Roborock devices."""
14
-
15
- command = RoborockCommand.GET_CLEAN_SUMMARY
16
-
17
- async def refresh(self) -> None:
18
- """Refresh the clean summary data and last clean record.
19
-
20
- Assumes that the clean summary has already been fetched.
21
- """
22
- await super().refresh()
23
- if not self.records:
24
- _LOGGER.debug("No clean records available in clean summary.")
25
- self.last_clean_record = None
26
- return
27
- last_record_id = self.records[0]
28
- self.last_clean_record = await self.get_clean_record(last_record_id)
11
+ class CleanSummaryConverter(common.V1TraitDataConverter):
12
+ """Converter for CleanSummaryWithDetail objects."""
29
13
 
30
- @classmethod
31
- def _parse_type_response(cls, response: common.V1ResponseData) -> Self:
14
+ def convert(self, response: common.V1ResponseData) -> RoborockBase:
32
15
  """Parse the response from the device into a CleanSummary."""
33
16
  if isinstance(response, dict):
34
- return cls.from_dict(response)
17
+ return CleanSummaryWithDetail.from_dict(response)
35
18
  elif isinstance(response, list):
36
19
  clean_time, clean_area, clean_count, records = unpack_list(response, 4)
37
- return cls(
20
+ return CleanSummaryWithDetail(
38
21
  clean_time=clean_time,
39
22
  clean_area=clean_area,
40
23
  clean_count=clean_count,
41
24
  records=records,
42
25
  )
43
26
  elif isinstance(response, int):
44
- return cls(clean_time=response)
27
+ return CleanSummaryWithDetail(clean_time=response)
45
28
  raise ValueError(f"Unexpected clean summary format: {response!r}")
46
29
 
47
- async def get_clean_record(self, record_id: int) -> CleanRecord:
48
- """Load a specific clean record by ID."""
49
- response = await self.rpc_channel.send_command(RoborockCommand.GET_CLEAN_RECORD, params=[record_id])
50
- return self._parse_clean_record_response(response)
51
30
 
52
- @classmethod
53
- def _parse_clean_record_response(cls, response: common.V1ResponseData) -> CleanRecord:
31
+ class CleanRecordConverter(common.V1TraitDataConverter):
32
+ """Convert server responses to a CleanRecord."""
33
+
34
+ def convert(self, response: common.V1ResponseData) -> CleanRecord:
54
35
  """Parse the response from the device into a CleanRecord."""
55
36
  if isinstance(response, list) and len(response) == 1:
56
37
  response = response[0]
@@ -81,3 +62,29 @@ class CleanSummaryTrait(CleanSummaryWithDetail, common.V1TraitMixin):
81
62
  begin, end, duration, area = unpack_list(response, 4)
82
63
  return CleanRecord(begin=begin, end=end, duration=duration, area=area)
83
64
  raise ValueError(f"Unexpected clean record format: {response!r}")
65
+
66
+
67
+ class CleanSummaryTrait(CleanSummaryWithDetail, common.V1TraitMixin):
68
+ """Trait for managing the clean summary of Roborock devices."""
69
+
70
+ command = RoborockCommand.GET_CLEAN_SUMMARY
71
+ converter = CleanSummaryConverter()
72
+ clean_record_converter = CleanRecordConverter()
73
+
74
+ async def refresh(self) -> None:
75
+ """Refresh the clean summary data and last clean record.
76
+
77
+ Assumes that the clean summary has already been fetched.
78
+ """
79
+ await super().refresh()
80
+ if not self.records:
81
+ _LOGGER.debug("No clean records available in clean summary.")
82
+ self.last_clean_record = None
83
+ return
84
+ last_record_id = self.records[0]
85
+ self.last_clean_record = await self.get_clean_record(last_record_id)
86
+
87
+ async def get_clean_record(self, record_id: int) -> CleanRecord:
88
+ """Load a specific clean record by ID."""
89
+ response = await self.rpc_channel.send_command(RoborockCommand.GET_CLEAN_RECORD, params=[record_id])
90
+ return self.clean_record_converter.convert(response)
@@ -5,8 +5,8 @@ This is an internal library and should not be used directly by consumers.
5
5
 
6
6
  import logging
7
7
  from abc import ABC, abstractmethod
8
- from dataclasses import dataclass, fields
9
- from typing import ClassVar, Self
8
+ from dataclasses import fields
9
+ from typing import ClassVar
10
10
 
11
11
  from roborock.data import RoborockBase
12
12
  from roborock.protocols.v1_protocol import V1RpcChannel
@@ -14,10 +14,24 @@ from roborock.roborock_typing import RoborockCommand
14
14
 
15
15
  _LOGGER = logging.getLogger(__name__)
16
16
 
17
+
17
18
  V1ResponseData = dict | list | int | str
18
19
 
19
20
 
20
- @dataclass
21
+ class V1TraitDataConverter(ABC):
22
+ """Converts responses to RoborockBase objects.
23
+
24
+ This is an internal class and should not be used directly by consumers.
25
+ """
26
+
27
+ @abstractmethod
28
+ def convert(self, response: V1ResponseData) -> RoborockBase:
29
+ """Convert the values to a dict that can be parsed as a RoborockBase."""
30
+
31
+ def __repr__(self) -> str:
32
+ return self.__class__.__name__
33
+
34
+
21
35
  class V1TraitMixin(ABC):
22
36
  """Base model that supports v1 traits.
23
37
 
@@ -42,37 +56,13 @@ class V1TraitMixin(ABC):
42
56
  """
43
57
 
44
58
  command: ClassVar[RoborockCommand]
59
+ """The RoborockCommand used to fetch the trait data from the device (internal only)."""
45
60
 
46
- @classmethod
47
- def _parse_type_response(cls, response: V1ResponseData) -> RoborockBase:
48
- """Parse the response from the device into a a RoborockBase.
49
-
50
- Subclasses should override this method to implement custom parsing
51
- logic as needed.
52
- """
53
- if not issubclass(cls, RoborockBase):
54
- raise NotImplementedError(f"Trait {cls} does not implement RoborockBase")
55
- # Subclasses can override to implement custom parsing logic
56
- if isinstance(response, list):
57
- response = response[0]
58
- if not isinstance(response, dict):
59
- raise ValueError(f"Unexpected {cls} response format: {response!r}")
60
- return cls.from_dict(response)
61
-
62
- def _parse_response(self, response: V1ResponseData) -> RoborockBase:
63
- """Parse the response from the device into a a RoborockBase.
64
-
65
- This is used by subclasses that want to override the class
66
- behavior with instance-specific data.
67
- """
68
- return self._parse_type_response(response)
69
-
70
- def __post_init__(self) -> None:
71
- """Post-initialization to set up the RPC channel.
61
+ converter: V1TraitDataConverter
62
+ """The converter used to parse the response from the device (internal only)."""
72
63
 
73
- This is called automatically after the dataclass is initialized by the
74
- device setup code.
75
- """
64
+ def __init__(self) -> None:
65
+ """Initialize the V1TraitMixin."""
76
66
  self._rpc_channel = None
77
67
 
78
68
  @property
@@ -85,32 +75,42 @@ class V1TraitMixin(ABC):
85
75
  async def refresh(self) -> None:
86
76
  """Refresh the contents of this trait."""
87
77
  response = await self.rpc_channel.send_command(self.command)
88
- new_data = self._parse_response(response)
89
- if not isinstance(new_data, RoborockBase):
90
- raise ValueError(f"Internal error, unexpected response type: {new_data!r}")
91
- _LOGGER.debug("Refreshed %s: %s", self.__class__.__name__, new_data)
92
- self._update_trait_values(new_data)
93
-
94
- def _update_trait_values(self, new_data: RoborockBase) -> None:
95
- """Update the values of this trait from another instance."""
96
- for field in fields(new_data):
97
- new_value = getattr(new_data, field.name, None)
98
- setattr(self, field.name, new_value)
99
-
100
-
101
- def _get_value_field(clazz: type[V1TraitMixin]) -> str:
102
- """Get the name of the field marked as the main value of the RoborockValueBase."""
103
- value_fields = [field.name for field in fields(clazz) if field.metadata.get("roborock_value", False)]
104
- if len(value_fields) != 1:
105
- raise ValueError(
106
- f"RoborockValueBase subclass {clazz} must have exactly one field marked as roborock_value, "
107
- f" but found: {value_fields}"
108
- )
109
- return value_fields[0]
110
-
111
-
112
- @dataclass(init=False, kw_only=True)
113
- class RoborockValueBase(V1TraitMixin, RoborockBase):
78
+ new_data = self.converter.convert(response)
79
+ merge_trait_values(self, new_data) # type: ignore[arg-type]
80
+
81
+
82
+ def merge_trait_values(target: RoborockBase, new_object: RoborockBase) -> bool:
83
+ """Update the target object with set fields in new_object."""
84
+ updated = False
85
+ for field in fields(new_object):
86
+ old_value = getattr(target, field.name, None)
87
+ new_value = getattr(new_object, field.name, None)
88
+ if new_value != old_value:
89
+ setattr(target, field.name, new_value)
90
+ updated = True
91
+ return updated
92
+
93
+
94
+ class DefaultConverter(V1TraitDataConverter):
95
+ """Converts responses to RoborockBase objects."""
96
+
97
+ def __init__(self, dataclass_type: type[RoborockBase]) -> None:
98
+ """Initialize the converter."""
99
+ self._dataclass_type = dataclass_type
100
+
101
+ def convert(self, response: V1ResponseData) -> RoborockBase:
102
+ """Convert the values to a dict that can be parsed as a RoborockBase.
103
+
104
+ Subclasses can override to implement custom parsing logic
105
+ """
106
+ if isinstance(response, list):
107
+ response = response[0]
108
+ if not isinstance(response, dict):
109
+ raise ValueError(f"Unexpected {self._dataclass_type.__name__} response format: {response!r}")
110
+ return self._dataclass_type.from_dict(response)
111
+
112
+
113
+ class SingleValueConverter(DefaultConverter):
114
114
  """Base class for traits that represent a single value.
115
115
 
116
116
  This class is intended to be subclassed by traits that represent a single
@@ -119,15 +119,18 @@ class RoborockValueBase(V1TraitMixin, RoborockBase):
119
119
  represents the main value of the trait.
120
120
  """
121
121
 
122
- @classmethod
123
- def _parse_response(cls, response: V1ResponseData) -> Self:
122
+ def __init__(self, dataclass_type: type[RoborockBase], value_field: str) -> None:
123
+ """Initialize the converter."""
124
+ super().__init__(dataclass_type)
125
+ self._value_field = value_field
126
+
127
+ def convert(self, response: V1ResponseData) -> RoborockBase:
124
128
  """Parse the response from the device into a RoborockValueBase."""
125
129
  if isinstance(response, list):
126
130
  response = response[0]
127
131
  if not isinstance(response, int):
128
132
  raise ValueError(f"Unexpected response format: {response!r}")
129
- value_field = _get_value_field(cls)
130
- return cls(**{value_field: response})
133
+ return super().convert({self._value_field: response})
131
134
 
132
135
 
133
136
  class RoborockSwitchBase(ABC):
@@ -41,6 +41,7 @@ class ConsumableTrait(Consumable, common.V1TraitMixin):
41
41
  """
42
42
 
43
43
  command = RoborockCommand.GET_CONSUMABLE
44
+ converter = common.DefaultConverter(Consumable)
44
45
 
45
46
  async def reset_consumable(self, consumable: ConsumableAttribute) -> None:
46
47
  """Reset a specific consumable attribute on the device."""
@@ -8,15 +8,37 @@ from roborock.devices.traits.v1 import common
8
8
  from roborock.roborock_typing import RoborockCommand
9
9
 
10
10
 
11
+ class DeviceTraitsConverter(common.V1TraitDataConverter):
12
+ """Converter for APP_GET_INIT_STATUS responses into DeviceFeatures."""
13
+
14
+ def __init__(self, product: HomeDataProduct) -> None:
15
+ """Initialize DeviceTraitsConverter."""
16
+ self._product = product
17
+
18
+ def convert(self, response: common.V1ResponseData) -> DeviceFeatures:
19
+ """Parse an APP_GET_INIT_STATUS response into a DeviceFeatures instance."""
20
+ if not isinstance(response, list):
21
+ raise ValueError(f"Unexpected AppInitStatus response format: {type(response)}: {response!r}")
22
+ app_status = AppInitStatus.from_dict(response[0])
23
+ return DeviceFeatures.from_feature_flags(
24
+ new_feature_info=app_status.new_feature_info,
25
+ new_feature_info_str=app_status.new_feature_info_str,
26
+ feature_info=app_status.feature_info,
27
+ product_nickname=self._product.product_nickname,
28
+ )
29
+
30
+
11
31
  class DeviceFeaturesTrait(DeviceFeatures, common.V1TraitMixin):
12
32
  """Trait for managing supported features on Roborock devices."""
13
33
 
14
34
  command = RoborockCommand.APP_GET_INIT_STATUS
35
+ converter: DeviceTraitsConverter
15
36
 
16
37
  def __init__(self, product: HomeDataProduct, device_cache: DeviceCache) -> None: # pylint: disable=super-init-not-called
17
38
  """Initialize DeviceFeaturesTrait."""
39
+ common.V1TraitMixin.__init__(self)
40
+ self.converter = DeviceTraitsConverter(product)
18
41
  self._product = product
19
- self._nickname = product.product_nickname
20
42
  self._device_cache = device_cache
21
43
  # All fields of DeviceFeatures are required. Initialize them to False
22
44
  # so we have some known state.
@@ -54,21 +76,9 @@ class DeviceFeaturesTrait(DeviceFeatures, common.V1TraitMixin):
54
76
  """
55
77
  cache_data = await self._device_cache.get()
56
78
  if cache_data.device_features is not None:
57
- self._update_trait_values(cache_data.device_features)
79
+ common.merge_trait_values(self, cache_data.device_features)
58
80
  return
59
81
  # Save cached device features
60
82
  await super().refresh()
61
83
  cache_data.device_features = self
62
84
  await self._device_cache.set(cache_data)
63
-
64
- def _parse_response(self, response: common.V1ResponseData) -> DeviceFeatures:
65
- """Parse the response from the device into a MapContentTrait instance."""
66
- if not isinstance(response, list):
67
- raise ValueError(f"Unexpected AppInitStatus response format: {type(response)}")
68
- app_status = AppInitStatus.from_dict(response[0])
69
- return DeviceFeatures.from_feature_flags(
70
- new_feature_info=app_status.new_feature_info,
71
- new_feature_info_str=app_status.new_feature_info_str,
72
- feature_info=app_status.feature_info,
73
- product_nickname=self._nickname,
74
- )
@@ -9,6 +9,7 @@ class DoNotDisturbTrait(DnDTimer, common.V1TraitMixin, common.RoborockSwitchBase
9
9
  """Trait for managing Do Not Disturb (DND) settings on Roborock devices."""
10
10
 
11
11
  command = RoborockCommand.GET_DND_TIMER
12
+ converter = common.DefaultConverter(DnDTimer)
12
13
 
13
14
  @property
14
15
  def is_on(self) -> bool:
@@ -10,4 +10,5 @@ class DustCollectionModeTrait(DustCollectionMode, common.V1TraitMixin):
10
10
  """Trait for dust collection mode."""
11
11
 
12
12
  command = RoborockCommand.GET_DUST_COLLECTION_MODE
13
+ converter = common.DefaultConverter(DustCollectionMode)
13
14
  requires_dock_type = is_valid_dock
@@ -9,6 +9,7 @@ class FlowLedStatusTrait(FlowLedStatus, common.V1TraitMixin, common.RoborockSwit
9
9
  """Trait for controlling the Flow LED status of a Roborock device."""
10
10
 
11
11
  command = RoborockCommand.GET_FLOW_LED_STATUS
12
+ converter = common.DefaultConverter(FlowLedStatus)
12
13
  requires_feature = "is_flow_led_setting_supported"
13
14
 
14
15
  @property
@@ -18,7 +18,6 @@ the current map's information and room names as needed.
18
18
  import asyncio
19
19
  import base64
20
20
  import logging
21
- from typing import Self
22
21
 
23
22
  from roborock.data import CombinedMapInfo, MultiMapsListMapInfo, NamedRoomMapping, RoborockBase
24
23
  from roborock.data.v1.v1_code_mappings import RoborockStateCode
@@ -41,6 +40,7 @@ class HomeTrait(RoborockBase, common.V1TraitMixin):
41
40
  """Trait that represents a full view of the home layout."""
42
41
 
43
42
  command = RoborockCommand.GET_MAP_V1 # This is not used
43
+ converter = common.DefaultConverter(RoborockBase) # Not used
44
44
 
45
45
  def __init__(
46
46
  self,
@@ -93,7 +93,7 @@ class HomeTrait(RoborockBase, common.V1TraitMixin):
93
93
  self._discovery_completed = True
94
94
  try:
95
95
  self._home_map_content = {
96
- k: self._map_content.parse_map_content(base64.b64decode(v))
96
+ k: self._map_content.converter.parse_map_content(base64.b64decode(v))
97
97
  for k, v in (device_cache_data.home_map_content_base64 or {}).items()
98
98
  }
99
99
  except (ValueError, RoborockException) as ex:
@@ -233,10 +233,6 @@ class HomeTrait(RoborockBase, common.V1TraitMixin):
233
233
  """Returns the map content for all cached maps."""
234
234
  return self._home_map_content
235
235
 
236
- def _parse_response(self, response: common.V1ResponseData) -> Self:
237
- """This trait does not parse responses directly."""
238
- raise NotImplementedError("HomeTrait does not support direct command responses")
239
-
240
236
  async def _update_home_cache(
241
237
  self, home_map_info: dict[int, CombinedMapInfo], home_map_content: dict[int, MapContent]
242
238
  ) -> None:
@@ -5,10 +5,28 @@ from roborock.roborock_typing import RoborockCommand
5
5
  from .common import V1ResponseData
6
6
 
7
7
 
8
+ class LedStatusConverter(common.V1TraitDataConverter):
9
+ """Converter for LedStatus."""
10
+
11
+ def convert(self, response: V1ResponseData) -> LedStatus:
12
+ """Parse the response from the device into a a RoborockBase.
13
+
14
+ Subclasses should override this method to implement custom parsing
15
+ logic as needed.
16
+ """
17
+ if not isinstance(response, list):
18
+ raise ValueError(f"Unexpected LedStatus response format: {response!r}")
19
+ response = response[0]
20
+ if not isinstance(response, int):
21
+ raise ValueError(f"Unexpected LedStatus response format: {response!r}")
22
+ return LedStatus.from_dict({"status": response})
23
+
24
+
8
25
  class LedStatusTrait(LedStatus, common.V1TraitMixin, common.RoborockSwitchBase):
9
26
  """Trait for controlling the LED status of a Roborock device."""
10
27
 
11
28
  command = RoborockCommand.GET_LED_STATUS
29
+ converter = LedStatusConverter()
12
30
  requires_feature = "is_led_status_switch_supported"
13
31
 
14
32
  @property
@@ -27,17 +45,3 @@ class LedStatusTrait(LedStatus, common.V1TraitMixin, common.RoborockSwitchBase):
27
45
  await self.rpc_channel.send_command(RoborockCommand.SET_LED_STATUS, params=[0])
28
46
  # Optimistic update to avoid an extra refresh
29
47
  self.status = 0
30
-
31
- @classmethod
32
- def _parse_type_response(cls, response: V1ResponseData) -> LedStatus:
33
- """Parse the response from the device into a a RoborockBase.
34
-
35
- Subclasses should override this method to implement custom parsing
36
- logic as needed.
37
- """
38
- if not isinstance(response, list):
39
- raise ValueError(f"Unexpected {cls} response format: {response!r}")
40
- response = response[0]
41
- if not isinstance(response, int):
42
- raise ValueError(f"Unexpected {cls} response format: {response!r}")
43
- return cls.from_dict({"status": response})
@@ -40,19 +40,15 @@ class MapContent(RoborockBase):
40
40
  return f"MapContent(image_content={img!r}, map_data={self.map_data!r})"
41
41
 
42
42
 
43
- @common.map_rpc_channel
44
- class MapContentTrait(MapContent, common.V1TraitMixin):
45
- """Trait for fetching the map content."""
46
-
47
- command = RoborockCommand.GET_MAP_V1
43
+ class MapContentConverter(common.V1TraitDataConverter):
44
+ """Convert map response data to MapContent."""
48
45
 
49
- def __init__(self, map_parser_config: MapParserConfig | None = None) -> None:
50
- """Initialize MapContentTrait."""
51
- super().__init__()
52
- self._map_parser = MapParser(map_parser_config or MapParserConfig())
46
+ def __init__(self, map_parser: MapParser) -> None:
47
+ """Initialize MapContentConverter."""
48
+ self._map_parser = map_parser
53
49
 
54
- def _parse_response(self, response: common.V1ResponseData) -> MapContent:
55
- """Parse the response from the device into a MapContentTrait instance."""
50
+ def convert(self, response: common.V1ResponseData) -> MapContent:
51
+ """Parse the response from the device into a MapContent instance."""
56
52
  if not isinstance(response, bytes):
57
53
  raise ValueError(f"Unexpected MapContentTrait response format: {type(response)}")
58
54
  return self.parse_map_content(response)
@@ -81,3 +77,16 @@ class MapContentTrait(MapContent, common.V1TraitMixin):
81
77
  map_data=parsed_data.map_data,
82
78
  raw_api_response=response,
83
79
  )
80
+
81
+
82
+ @common.map_rpc_channel
83
+ class MapContentTrait(MapContent, common.V1TraitMixin):
84
+ """Trait for fetching the map content."""
85
+
86
+ command = RoborockCommand.GET_MAP_V1
87
+ converter: MapContentConverter
88
+
89
+ def __init__(self, map_parser_config: MapParserConfig | None = None) -> None:
90
+ """Initialize MapContentTrait."""
91
+ super().__init__()
92
+ self.converter = MapContentConverter(MapParser(map_parser_config or MapParserConfig()))
@@ -6,7 +6,6 @@ base container datatypes to add additional fields.
6
6
  """
7
7
 
8
8
  import logging
9
- from typing import Self
10
9
 
11
10
  from roborock.data import MultiMapsList, MultiMapsListMapInfo
12
11
  from roborock.devices.traits.v1 import common
@@ -17,6 +16,24 @@ from .status import StatusTrait
17
16
  _LOGGER = logging.getLogger(__name__)
18
17
 
19
18
 
19
+ class MultiMapsListConverter(common.V1TraitDataConverter):
20
+ """Converters responses to MultiMapsList."""
21
+
22
+ def convert(self, response: common.V1ResponseData) -> MultiMapsList:
23
+ """Parse the response from the device into a MapsTrait instance.
24
+
25
+ This overrides the base implementation to handle the specific
26
+ response format for the multi maps list. This is needed because we have
27
+ a custom constructor that requires the StatusTrait.
28
+ """
29
+ if not isinstance(response, list):
30
+ raise ValueError(f"Unexpected MapsTrait response format: {response!r}")
31
+ response = response[0]
32
+ if not isinstance(response, dict):
33
+ raise ValueError(f"Unexpected MapsTrait response format: {response!r}")
34
+ return MultiMapsList.from_dict(response)
35
+
36
+
20
37
  @common.mqtt_rpc_channel
21
38
  class MapsTrait(MultiMapsList, common.V1TraitMixin):
22
39
  """Trait for managing the maps of Roborock devices.
@@ -34,6 +51,7 @@ class MapsTrait(MultiMapsList, common.V1TraitMixin):
34
51
  """
35
52
 
36
53
  command = RoborockCommand.GET_MULTI_MAPS_LIST
54
+ converter = MultiMapsListConverter()
37
55
 
38
56
  def __init__(self, status_trait: StatusTrait) -> None:
39
57
  """Initialize the MapsTrait.
@@ -64,17 +82,3 @@ class MapsTrait(MultiMapsList, common.V1TraitMixin):
64
82
  await self.rpc_channel.send_command(RoborockCommand.LOAD_MULTI_MAP, params=[map_flag])
65
83
  # Refresh our status to make sure it reflects the new map
66
84
  await self._status_trait.refresh()
67
-
68
- def _parse_response(self, response: common.V1ResponseData) -> Self:
69
- """Parse the response from the device into a MapsTrait instance.
70
-
71
- This overrides the base implementation to handle the specific
72
- response format for the multi maps list. This is needed because we have
73
- a custom constructor that requires the StatusTrait.
74
- """
75
- if not isinstance(response, list):
76
- raise ValueError(f"Unexpected MapsTrait response format: {response!r}")
77
- response = response[0]
78
- if not isinstance(response, dict):
79
- raise ValueError(f"Unexpected MapsTrait response format: {response!r}")
80
- return MultiMapsList.from_dict(response)
@@ -12,6 +12,16 @@ from roborock.roborock_typing import RoborockCommand
12
12
  _LOGGER = logging.getLogger(__name__)
13
13
 
14
14
 
15
+ class NetworkInfoConverter(common.V1TraitDataConverter):
16
+ """Converter for NetworkInfo objects."""
17
+
18
+ def convert(self, response: common.V1ResponseData) -> NetworkInfo:
19
+ """Parse the response from the device into a NetworkInfoConverter instance."""
20
+ if not isinstance(response, dict):
21
+ raise ValueError(f"Unexpected NetworkInfoTrait response format: {response!r}")
22
+ return NetworkInfo.from_dict(response)
23
+
24
+
15
25
  class NetworkInfoTrait(NetworkInfo, common.V1TraitMixin):
16
26
  """Trait for device network information.
17
27
 
@@ -23,6 +33,7 @@ class NetworkInfoTrait(NetworkInfo, common.V1TraitMixin):
23
33
  """
24
34
 
25
35
  command = RoborockCommand.GET_NETWORK_INFO
36
+ converter = NetworkInfoConverter()
26
37
 
27
38
  def __init__(self, device_uid: str, device_cache: DeviceCache) -> None: # pylint: disable=super-init-not-called
28
39
  """Initialize the trait."""
@@ -36,7 +47,7 @@ class NetworkInfoTrait(NetworkInfo, common.V1TraitMixin):
36
47
  device_cache_data = await self._device_cache.get()
37
48
  if device_cache_data.network_info:
38
49
  _LOGGER.debug("Using cached network info for device %s", self._device_uid)
39
- self._update_trait_values(device_cache_data.network_info)
50
+ common.merge_trait_values(self, device_cache_data.network_info)
40
51
  return
41
52
 
42
53
  # Load from device if not in cache
@@ -47,9 +58,3 @@ class NetworkInfoTrait(NetworkInfo, common.V1TraitMixin):
47
58
  device_cache_data = await self._device_cache.get()
48
59
  device_cache_data.network_info = self
49
60
  await self._device_cache.set(device_cache_data)
50
-
51
- def _parse_response(self, response: common.V1ResponseData) -> NetworkInfo:
52
- """Parse the response from the device into a NetworkInfo."""
53
- if not isinstance(response, dict):
54
- raise ValueError(f"Unexpected NetworkInfoTrait response format: {response!r}")
55
- return NetworkInfo.from_dict(response)
@@ -25,11 +25,63 @@ class Rooms(RoborockBase):
25
25
  return {}
26
26
  return {room.segment_id: room for room in self.rooms}
27
27
 
28
+ def with_room_names(self, name_map: dict[str, str]) -> "Rooms":
29
+ """Create a new Rooms object with updated room names."""
30
+ return Rooms(
31
+ rooms=[
32
+ NamedRoomMapping(
33
+ segment_id=room.segment_id,
34
+ iot_id=room.iot_id,
35
+ raw_name=name_map.get(room.iot_id),
36
+ )
37
+ for room in self.rooms or []
38
+ ]
39
+ )
40
+
41
+
42
+ class RoomsConverter(common.V1TraitDataConverter):
43
+ """Converts response objects to Rooms."""
44
+
45
+ def convert(self, response: common.V1ResponseData) -> Rooms:
46
+ """Parse the response from the device into a list of NamedRoomMapping."""
47
+ if not isinstance(response, list):
48
+ raise ValueError(f"Unexpected RoomsTrait response format: {response!r}")
49
+ segment_map = self.extract_segment_map(response)
50
+ return Rooms(
51
+ rooms=[NamedRoomMapping(segment_id=segment_id, iot_id=iot_id) for segment_id, iot_id in segment_map.items()]
52
+ )
53
+
54
+ @staticmethod
55
+ def extract_segment_map(response: list) -> dict[int, str]:
56
+ """Extract a segment_id -> iot_id mapping from the response.
57
+
58
+ The response format can be either a flat list of [segment_id, iot_id] or a
59
+ list of lists, where each inner list is a pair of [segment_id, iot_id]. This
60
+ function normalizes the response into a dict of segment_id to iot_id.
61
+
62
+ NOTE: We currently only partial samples of the room mapping formats, so
63
+ improving test coverage with samples from a real device with this format
64
+ would be helpful.
65
+ """
66
+ if len(response) == 2 and not isinstance(response[0], list):
67
+ segment_id, iot_id = response[0], response[1]
68
+ return {segment_id: str(iot_id)}
69
+
70
+ segment_map: dict[int, str] = {}
71
+ for part in response:
72
+ if not isinstance(part, list) or len(part) < 2:
73
+ _LOGGER.warning("Unexpected room mapping entry format: %r", part)
74
+ continue
75
+ segment_id, iot_id = part[0], part[1]
76
+ segment_map[segment_id] = str(iot_id)
77
+ return segment_map
78
+
28
79
 
29
80
  class RoomsTrait(Rooms, common.V1TraitMixin):
30
81
  """Trait for managing the room mappings of Roborock devices."""
31
82
 
32
83
  command = RoborockCommand.GET_ROOM_MAPPING
84
+ converter = RoomsConverter()
33
85
 
34
86
  def __init__(self, home_data: HomeData, web_api: UserWebApiClient) -> None:
35
87
  """Initialize the RoomsTrait."""
@@ -44,7 +96,7 @@ class RoomsTrait(Rooms, common.V1TraitMixin):
44
96
  if not isinstance(response, list):
45
97
  raise ValueError(f"Unexpected RoomsTrait response format: {response!r}")
46
98
 
47
- segment_map = _extract_segment_map(response)
99
+ segment_map = RoomsConverter.extract_segment_map(response)
48
100
  # Track all iot ids seen before. Refresh the room list when new ids are found.
49
101
  new_iot_ids = set(segment_map.values()) - set(self._home_data.rooms_map.keys())
50
102
  if new_iot_ids - self._discovered_iot_ids:
@@ -54,22 +106,9 @@ class RoomsTrait(Rooms, common.V1TraitMixin):
54
106
  self._home_data.rooms = updated_rooms
55
107
  self._discovered_iot_ids.update(new_iot_ids)
56
108
 
57
- new_data = self._parse_rooms(segment_map, self._home_data.rooms_name_map)
58
- self._update_trait_values(new_data)
59
- _LOGGER.debug("Refreshed %s: %s", self.__class__.__name__, new_data)
60
-
61
- @staticmethod
62
- def _parse_rooms(
63
- segment_map: dict[int, str],
64
- name_map: dict[str, str],
65
- ) -> Rooms:
66
- """Parse the response from the device into a list of NamedRoomMapping."""
67
- return Rooms(
68
- rooms=[
69
- NamedRoomMapping(segment_id=segment_id, iot_id=iot_id, raw_name=name_map.get(iot_id))
70
- for segment_id, iot_id in segment_map.items()
71
- ]
72
- )
109
+ rooms = self.converter.convert(response)
110
+ rooms = rooms.with_room_names(self._home_data.rooms_name_map)
111
+ common.merge_trait_values(self, rooms)
73
112
 
74
113
  async def _refresh_rooms(self) -> list[HomeDataRoom]:
75
114
  """Fetch the latest rooms from the web API."""
@@ -78,28 +117,3 @@ class RoomsTrait(Rooms, common.V1TraitMixin):
78
117
  except Exception:
79
118
  _LOGGER.debug("Failed to fetch rooms from web API", exc_info=True)
80
119
  return []
81
-
82
-
83
- def _extract_segment_map(response: list) -> dict[int, str]:
84
- """Extract a segment_id -> iot_id mapping from the response.
85
-
86
- The response format can be either a flat list of [segment_id, iot_id] or a
87
- list of lists, where each inner list is a pair of [segment_id, iot_id]. This
88
- function normalizes the response into a dict of segment_id to iot_id.
89
-
90
- NOTE: We currently only partial samples of the room mapping formats, so
91
- improving test coverage with samples from a real device with this format
92
- would be helpful.
93
- """
94
- if len(response) == 2 and not isinstance(response[0], list):
95
- segment_id, iot_id = response[0], response[1]
96
- return {segment_id: str(iot_id)}
97
-
98
- segment_map: dict[int, str] = {}
99
- for part in response:
100
- if not isinstance(part, list) or len(part) < 2:
101
- _LOGGER.warning("Unexpected room mapping entry format: %r", part)
102
- continue
103
- segment_id, iot_id = part[0], part[1]
104
- segment_map[segment_id] = str(iot_id)
105
- return segment_map
@@ -10,4 +10,5 @@ class SmartWashParamsTrait(SmartWashParams, common.V1TraitMixin):
10
10
  """Trait for smart wash parameters."""
11
11
 
12
12
  command = RoborockCommand.GET_SMART_WASH_PARAMS
13
+ converter = common.DefaultConverter(SmartWashParams)
13
14
  requires_dock_type = is_wash_n_fill_dock
@@ -1,5 +1,4 @@
1
1
  from functools import cached_property
2
- from typing import Self
3
2
 
4
3
  from roborock import (
5
4
  CleanRoutes,
@@ -43,6 +42,7 @@ class StatusTrait(StatusV2, common.V1TraitMixin):
43
42
  """
44
43
 
45
44
  command = RoborockCommand.GET_STATUS
45
+ converter = common.DefaultConverter(StatusV2)
46
46
 
47
47
  def __init__(self, device_feature_trait: DeviceFeaturesTrait, region: str | None = None) -> None:
48
48
  """Initialize the StatusTrait."""
@@ -91,11 +91,3 @@ class StatusTrait(StatusV2, common.V1TraitMixin):
91
91
  if self.mop_mode is None:
92
92
  return None
93
93
  return self.mop_route_mapping.get(self.mop_mode)
94
-
95
- def _parse_response(self, response: common.V1ResponseData) -> Self:
96
- """Parse the response from the device into a StatusV2-based status object."""
97
- if isinstance(response, list):
98
- response = response[0]
99
- if isinstance(response, dict):
100
- return StatusV2.from_dict(response)
101
- raise ValueError(f"Unexpected status format: {response!r}")
@@ -9,6 +9,7 @@ class ValleyElectricityTimerTrait(ValleyElectricityTimer, common.V1TraitMixin, c
9
9
  """Trait for managing Valley Electricity Timer settings on Roborock devices."""
10
10
 
11
11
  command = RoborockCommand.GET_VALLEY_ELECTRICITY_TIMER
12
+ converter = common.DefaultConverter(ValleyElectricityTimer)
12
13
  requires_feature = "is_supported_valley_electricity"
13
14
 
14
15
  @property
@@ -1,18 +1,15 @@
1
- from dataclasses import dataclass, field
1
+ from dataclasses import dataclass
2
2
 
3
+ from roborock.data.containers import RoborockBase
3
4
  from roborock.devices.traits.v1 import common
4
5
  from roborock.roborock_typing import RoborockCommand
5
6
 
6
- # TODO: This is currently the pattern for holding all the commands that hold a
7
- # single value, but it still seems too verbose. Maybe we can generate these
8
- # dynamically or somehow make them less code.
9
-
10
7
 
11
8
  @dataclass
12
- class SoundVolume(common.RoborockValueBase):
9
+ class SoundVolume(RoborockBase):
13
10
  """Dataclass for sound volume."""
14
11
 
15
- volume: int | None = field(default=None, metadata={"roborock_value": True})
12
+ volume: int | None = None
16
13
  """Sound volume level (0-100)."""
17
14
 
18
15
 
@@ -20,6 +17,7 @@ class SoundVolumeTrait(SoundVolume, common.V1TraitMixin):
20
17
  """Trait for controlling the sound volume of a Roborock device."""
21
18
 
22
19
  command = RoborockCommand.GET_SOUND_VOLUME
20
+ converter = common.SingleValueConverter(SoundVolume, "volume")
23
21
 
24
22
  async def set_volume(self, volume: int) -> None:
25
23
  """Set the sound volume of the device."""
@@ -14,6 +14,7 @@ class WashTowelModeTrait(WashTowelMode, common.V1TraitMixin):
14
14
  """Trait for wash towel mode."""
15
15
 
16
16
  command = RoborockCommand.GET_WASH_TOWEL_MODE
17
+ converter = common.DefaultConverter(WashTowelMode)
17
18
  requires_dock_type = is_wash_n_fill_dock
18
19
 
19
20
  def __init__(