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.
- {python_roborock-4.22.0 → python_roborock-4.23.0}/PKG-INFO +1 -1
- {python_roborock-4.22.0 → python_roborock-4.23.0}/pyproject.toml +1 -1
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/child_lock.py +1 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/clean_summary.py +37 -30
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/common.py +65 -62
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/consumeable.py +1 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/device_features.py +24 -14
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/do_not_disturb.py +1 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/dust_collection_mode.py +1 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/flow_led_status.py +1 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/home.py +2 -6
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/led_status.py +18 -14
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/map_content.py +20 -11
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/maps.py +19 -15
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/network_info.py +12 -7
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/rooms.py +56 -42
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/smart_wash_params.py +1 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/status.py +1 -9
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/valley_electricity_timer.py +1 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/volume.py +5 -7
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/wash_towel_mode.py +1 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/.gitignore +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/LICENSE +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/README.md +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/__init__.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/broadcast_protocol.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/callbacks.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/cli.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/const.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/__init__.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/b01_q10/__init__.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/b01_q7/__init__.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/code_mappings.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/containers.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/dyad/__init__.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/dyad/dyad_containers.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/v1/__init__.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/v1/v1_clean_modes.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/v1/v1_code_mappings.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/v1/v1_containers.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/zeo/__init__.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/zeo/zeo_containers.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/device_features.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/README.md +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/__init__.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/cache.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/device.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/device_manager.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/file_cache.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/rpc/__init__.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/rpc/a01_channel.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/rpc/b01_q10_channel.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/rpc/b01_q7_channel.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/rpc/v1_channel.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/__init__.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/a01/__init__.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/__init__.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q10/__init__.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q10/command.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q10/common.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q10/status.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q7/__init__.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q7/clean_summary.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q7/map.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/traits_mixin.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/__init__.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/command.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/routines.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/transport/__init__.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/transport/channel.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/transport/local_channel.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/transport/mqtt_channel.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/diagnostics.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/exceptions.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/map/__init__.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/map/map_parser.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/mqtt/health_manager.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/mqtt/roborock_session.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/mqtt/session.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/protocol.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/protocols/__init__.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/protocols/a01_protocol.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/protocols/b01_q10_protocol.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/protocols/b01_q7_protocol.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/protocols/v1_protocol.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/py.typed +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/roborock_message.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/roborock_typing.py +0 -0
- {python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/util.py +0 -0
- {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.
|
|
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.
|
|
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
|
{python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/clean_summary.py
RENAMED
|
@@ -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
|
|
13
|
-
"""
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
53
|
-
|
|
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
|
|
9
|
-
from typing import ClassVar
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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."""
|
{python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/device_features.py
RENAMED
|
@@ -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
|
-
|
|
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
|
-
)
|
{python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/do_not_disturb.py
RENAMED
|
@@ -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:
|
{python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/flow_led_status.py
RENAMED
|
@@ -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
|
-
|
|
44
|
-
|
|
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,
|
|
50
|
-
"""Initialize
|
|
51
|
-
|
|
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
|
|
55
|
-
"""Parse the response from the device into a
|
|
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)
|
{python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/network_info.py
RENAMED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
58
|
-
self.
|
|
59
|
-
|
|
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
|
|
@@ -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
|
|
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(
|
|
9
|
+
class SoundVolume(RoborockBase):
|
|
13
10
|
"""Dataclass for sound volume."""
|
|
14
11
|
|
|
15
|
-
volume: int | None =
|
|
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."""
|
{python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/wash_towel_mode.py
RENAMED
|
@@ -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__(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/b01_q10/b01_q10_code_mappings.py
RENAMED
|
File without changes
|
{python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/b01_q10/b01_q10_containers.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/data/b01_q7/b01_q7_code_mappings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q10/__init__.py
RENAMED
|
File without changes
|
{python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q10/command.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q7/__init__.py
RENAMED
|
File without changes
|
{python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q7/clean_summary.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/transport/local_channel.py
RENAMED
|
File without changes
|
{python_roborock-4.22.0 → python_roborock-4.23.0}/roborock/devices/transport/mqtt_channel.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|