python-roborock 2.59.0__tar.gz → 2.61.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 (79) hide show
  1. {python_roborock-2.59.0 → python_roborock-2.61.0}/PKG-INFO +1 -1
  2. {python_roborock-2.59.0 → python_roborock-2.61.0}/pyproject.toml +3 -3
  3. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/cli.py +28 -1
  4. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/device_features.py +23 -1
  5. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/cache.py +4 -1
  6. python_roborock-2.61.0/roborock/devices/traits/v1/__init__.py +260 -0
  7. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/traits/v1/child_lock.py +6 -1
  8. python_roborock-2.61.0/roborock/devices/traits/v1/clean_record.py +69 -0
  9. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/traits/v1/common.py +19 -1
  10. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/traits/v1/do_not_disturb.py +6 -1
  11. python_roborock-2.61.0/roborock/devices/traits/v1/dust_collection_mode.py +13 -0
  12. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/traits/v1/flow_led_status.py +6 -1
  13. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/traits/v1/led_status.py +6 -1
  14. python_roborock-2.61.0/roborock/devices/traits/v1/smart_wash_params.py +13 -0
  15. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/traits/v1/valley_electricity_timer.py +5 -0
  16. python_roborock-2.61.0/roborock/devices/traits/v1/wash_towel_mode.py +13 -0
  17. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/v1_channel.py +12 -4
  18. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/version_1_apis/roborock_client_v1.py +2 -11
  19. python_roborock-2.59.0/roborock/devices/traits/v1/__init__.py +0 -163
  20. {python_roborock-2.59.0 → python_roborock-2.61.0}/.gitignore +0 -0
  21. {python_roborock-2.59.0 → python_roborock-2.61.0}/LICENSE +0 -0
  22. {python_roborock-2.59.0 → python_roborock-2.61.0}/README.md +0 -0
  23. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/__init__.py +0 -0
  24. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/api.py +0 -0
  25. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/b01_containers.py +0 -0
  26. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/broadcast_protocol.py +0 -0
  27. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/callbacks.py +0 -0
  28. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/clean_modes.py +0 -0
  29. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/cloud_api.py +0 -0
  30. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/code_mappings.py +0 -0
  31. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/command_cache.py +0 -0
  32. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/const.py +0 -0
  33. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/containers.py +0 -0
  34. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/README.md +0 -0
  35. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/__init__.py +0 -0
  36. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/a01_channel.py +0 -0
  37. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/b01_channel.py +0 -0
  38. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/channel.py +0 -0
  39. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/device.py +0 -0
  40. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/device_manager.py +0 -0
  41. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/local_channel.py +0 -0
  42. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/mqtt_channel.py +0 -0
  43. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/traits/__init__.py +0 -0
  44. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/traits/a01/__init__.py +0 -0
  45. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/traits/b01/__init__.py +0 -0
  46. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/traits/traits_mixin.py +0 -0
  47. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
  48. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/traits/v1/command.py +0 -0
  49. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/traits/v1/consumeable.py +0 -0
  50. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/traits/v1/device_features.py +0 -0
  51. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/traits/v1/home.py +0 -0
  52. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/traits/v1/map_content.py +0 -0
  53. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/traits/v1/maps.py +0 -0
  54. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/traits/v1/rooms.py +0 -0
  55. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/traits/v1/status.py +0 -0
  56. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/traits/v1/volume.py +0 -0
  57. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/devices/v1_rpc_channel.py +0 -0
  58. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/exceptions.py +0 -0
  59. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/map/__init__.py +0 -0
  60. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/map/map_parser.py +0 -0
  61. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/mqtt/__init__.py +0 -0
  62. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/mqtt/roborock_session.py +0 -0
  63. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/mqtt/session.py +0 -0
  64. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/protocol.py +0 -0
  65. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/protocols/a01_protocol.py +0 -0
  66. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/protocols/b01_protocol.py +0 -0
  67. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/protocols/v1_protocol.py +0 -0
  68. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/py.typed +0 -0
  69. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/roborock_future.py +0 -0
  70. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/roborock_message.py +0 -0
  71. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/roborock_typing.py +0 -0
  72. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/util.py +0 -0
  73. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/version_1_apis/__init__.py +0 -0
  74. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
  75. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  76. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/version_a01_apis/__init__.py +0 -0
  77. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  78. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
  79. {python_roborock-2.59.0 → python_roborock-2.61.0}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-roborock
3
- Version: 2.59.0
3
+ Version: 2.61.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 = "2.59.0"
3
+ version = "2.61.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"
@@ -66,8 +66,8 @@ build-backend = "hatchling.build"
66
66
 
67
67
  [tool.semantic_release]
68
68
  branch = "main"
69
- version_toml = ["pyproject.toml:tool.poetry.version"]
70
- build_command = "pip install poetry && poetry build"
69
+ version_toml = ["pyproject.toml:project.version"]
70
+ build_command = "pip install uv && uv build"
71
71
 
72
72
  [tool.semantic_release.commit_parser_options]
73
73
  allowed_tags = [
@@ -119,6 +119,7 @@ class ConnectionCache(RoborockBase):
119
119
  home_data: HomeData | None = None
120
120
  network_info: dict[str, NetworkInfo] | None = None
121
121
  home_cache: dict[int, CombinedMapInfo] | None = None
122
+ trait_data: dict[str, Any] | None = None
122
123
 
123
124
 
124
125
  class DeviceConnectionManager:
@@ -267,6 +268,7 @@ class RoborockContext(Cache):
267
268
  home_data=connection_cache.home_data,
268
269
  network_info=connection_cache.network_info or {},
269
270
  home_cache=connection_cache.home_cache,
271
+ trait_data=connection_cache.trait_data or {},
270
272
  )
271
273
 
272
274
  async def set(self, value: CacheData) -> None:
@@ -276,6 +278,7 @@ class RoborockContext(Cache):
276
278
  connection_cache.home_data = value.home_data
277
279
  connection_cache.network_info = value.network_info
278
280
  connection_cache.home_cache = value.home_cache
281
+ connection_cache.trait_data = value.trait_data
279
282
  self.update(connection_cache)
280
283
 
281
284
 
@@ -401,9 +404,11 @@ async def _v1_trait(context: RoborockContext, device_id: str, display_func: Call
401
404
  device_manager = await context.get_device_manager()
402
405
  device = await device_manager.get_device(device_id)
403
406
  if device.v1_properties is None:
404
- raise RoborockException(f"Device {device.name} does not support V1 protocol")
407
+ raise RoborockUnsupportedFeature(f"Device {device.name} does not support V1 protocol")
405
408
  await device.v1_properties.discover_features()
406
409
  trait = display_func(device.v1_properties)
410
+ if trait is None:
411
+ raise RoborockUnsupportedFeature("Trait not supported by device")
407
412
  await trait.refresh()
408
413
  return trait
409
414
 
@@ -440,6 +445,26 @@ async def clean_summary(ctx, device_id: str):
440
445
  await _display_v1_trait(context, device_id, lambda v1: v1.clean_summary)
441
446
 
442
447
 
448
+ @session.command()
449
+ @click.option("--device_id", required=True)
450
+ @click.pass_context
451
+ @async_command
452
+ async def clean_record(ctx, device_id: str):
453
+ """Get device last clean record."""
454
+ context: RoborockContext = ctx.obj
455
+ await _display_v1_trait(context, device_id, lambda v1: v1.clean_record)
456
+
457
+
458
+ @session.command()
459
+ @click.option("--device_id", required=True)
460
+ @click.pass_context
461
+ @async_command
462
+ async def dock_summary(ctx, device_id: str):
463
+ """Get device dock summary."""
464
+ context: RoborockContext = ctx.obj
465
+ await _display_v1_trait(context, device_id, lambda v1: v1.dock_summary)
466
+
467
+
443
468
  @session.command()
444
469
  @click.option("--device_id", required=True)
445
470
  @click.pass_context
@@ -938,6 +963,8 @@ cli.add_command(session)
938
963
  cli.add_command(get_device_info)
939
964
  cli.add_command(update_docs)
940
965
  cli.add_command(clean_summary)
966
+ cli.add_command(clean_record)
967
+ cli.add_command(dock_summary)
941
968
  cli.add_command(volume)
942
969
  cli.add_command(set_volume)
943
970
  cli.add_command(maps)
@@ -4,7 +4,7 @@ from dataclasses import dataclass, field, fields
4
4
  from enum import IntEnum, StrEnum
5
5
  from typing import Any
6
6
 
7
- from .code_mappings import RoborockProductNickname
7
+ from .code_mappings import RoborockDockTypeCode, RoborockProductNickname
8
8
  from .containers import RoborockBase
9
9
 
10
10
 
@@ -630,3 +630,25 @@ class DeviceFeatures(RoborockBase):
630
630
  def get_supported_features(self) -> list[str]:
631
631
  """Returns a list of supported features (Primarily used for logging purposes)."""
632
632
  return [k for k, v in vars(self).items() if v]
633
+
634
+
635
+ WASH_N_FILL_DOCK_TYPES = [
636
+ RoborockDockTypeCode.empty_wash_fill_dock,
637
+ RoborockDockTypeCode.s8_dock,
638
+ RoborockDockTypeCode.p10_dock,
639
+ RoborockDockTypeCode.p10_pro_dock,
640
+ RoborockDockTypeCode.s8_maxv_ultra_dock,
641
+ RoborockDockTypeCode.qrevo_s_dock,
642
+ RoborockDockTypeCode.saros_r10_dock,
643
+ RoborockDockTypeCode.qrevo_curv_dock,
644
+ ]
645
+
646
+
647
+ def is_wash_n_fill_dock(dock_type: RoborockDockTypeCode) -> bool:
648
+ """Check if the dock type is a wash and fill dock."""
649
+ return dock_type in WASH_N_FILL_DOCK_TYPES
650
+
651
+
652
+ def is_valid_dock(dock_type: RoborockDockTypeCode) -> bool:
653
+ """Check if device supports a dock."""
654
+ return dock_type != RoborockDockTypeCode.no_dock
@@ -6,7 +6,7 @@ this interface to provide their own caching mechanism.
6
6
  """
7
7
 
8
8
  from dataclasses import dataclass, field
9
- from typing import Protocol
9
+ from typing import Any, Protocol
10
10
 
11
11
  from roborock.containers import CombinedMapInfo, HomeData, NetworkInfo
12
12
  from roborock.device_features import DeviceFeatures
@@ -28,6 +28,9 @@ class CacheData:
28
28
  device_features: DeviceFeatures | None = None
29
29
  """Device features information."""
30
30
 
31
+ trait_data: dict[str, Any] | None = None
32
+ """Trait-specific cached data used internally for caching device features."""
33
+
31
34
 
32
35
  class Cache(Protocol):
33
36
  """Protocol for a cache that can store and retrieve values."""
@@ -0,0 +1,260 @@
1
+ """Create traits for V1 devices.
2
+
3
+ Traits are modular components that encapsulate specific features of a Roborock
4
+ device. This module provides a factory function to create and initialize the
5
+ appropriate traits for V1 devices based on their capabilities. They can also
6
+ be considered groups of commands and parsing logic for that command.
7
+
8
+ Traits have a `refresh()` method that can be called to update their state
9
+ from the device. Some traits may also provide additional methods for modifying
10
+ the device state.
11
+
12
+ The most common pattern for a trait is to subclass `V1TraitMixin` and a `RoborockBase`
13
+ dataclass, and define a `command` class variable that specifies the `RoborockCommand`
14
+ used to fetch the trait data from the device. See `common.py` for more details
15
+ on common patterns used across traits.
16
+
17
+ There are some additional decorators in `common.py` that can be used to specify which
18
+ RPC channel to use for the trait (standard, MQTT/cloud, or map-specific).
19
+
20
+ - `@common.mqtt_rpc_channel` - Use the MQTT RPC channel for this trait.
21
+ - `@common.map_rpc_channel` - Use the map RPC channel for this trait.
22
+
23
+ There are also some attributes that specify device feature dependencies for
24
+ optional traits:
25
+
26
+ - `requires_feature` - The string name of the device feature that must be supported
27
+ for this trait to be enabled. See `DeviceFeaturesTrait` for a list of
28
+ available features.
29
+ - `requires_dock_type` - If set, this is a function that accepts a `RoborockDockTypeCode`
30
+ and returns a boolean indicating whether the trait is supported for that dock type.
31
+ """
32
+
33
+ import logging
34
+ from dataclasses import dataclass, field, fields
35
+ from typing import Any, get_args
36
+
37
+ from roborock.code_mappings import RoborockDockTypeCode
38
+ from roborock.containers import HomeData, HomeDataProduct
39
+ from roborock.devices.cache import Cache
40
+ from roborock.devices.traits import Trait
41
+ from roborock.devices.v1_rpc_channel import V1RpcChannel
42
+ from roborock.map.map_parser import MapParserConfig
43
+
44
+ from .child_lock import ChildLockTrait
45
+ from .clean_record import CleanRecordTrait
46
+ from .clean_summary import CleanSummaryTrait
47
+ from .command import CommandTrait
48
+ from .common import V1TraitMixin
49
+ from .consumeable import ConsumableTrait
50
+ from .device_features import DeviceFeaturesTrait
51
+ from .do_not_disturb import DoNotDisturbTrait
52
+ from .dust_collection_mode import DustCollectionModeTrait
53
+ from .flow_led_status import FlowLedStatusTrait
54
+ from .home import HomeTrait
55
+ from .led_status import LedStatusTrait
56
+ from .map_content import MapContentTrait
57
+ from .maps import MapsTrait
58
+ from .rooms import RoomsTrait
59
+ from .smart_wash_params import SmartWashParamsTrait
60
+ from .status import StatusTrait
61
+ from .valley_electricity_timer import ValleyElectricityTimerTrait
62
+ from .volume import SoundVolumeTrait
63
+ from .wash_towel_mode import WashTowelModeTrait
64
+
65
+ _LOGGER = logging.getLogger(__name__)
66
+
67
+ __all__ = [
68
+ "create",
69
+ "PropertiesApi",
70
+ "StatusTrait",
71
+ "DoNotDisturbTrait",
72
+ "CleanSummaryTrait",
73
+ "CleanRecordTrait",
74
+ "SoundVolumeTrait",
75
+ "MapsTrait",
76
+ "MapContentTrait",
77
+ "ConsumableTrait",
78
+ "HomeTrait",
79
+ "DeviceFeaturesTrait",
80
+ "CommandTrait",
81
+ "ChildLockTrait",
82
+ "FlowLedStatusTrait",
83
+ "LedStatusTrait",
84
+ "ValleyElectricityTimerTrait",
85
+ "DustCollectionModeTrait",
86
+ "WashTowelModeTrait",
87
+ "SmartWashParamsTrait",
88
+ ]
89
+
90
+
91
+ @dataclass
92
+ class PropertiesApi(Trait):
93
+ """Common properties for V1 devices.
94
+
95
+ This class holds all the traits that are common across all V1 devices.
96
+ """
97
+
98
+ # All v1 devices have these traits
99
+ status: StatusTrait
100
+ command: CommandTrait
101
+ dnd: DoNotDisturbTrait
102
+ clean_summary: CleanSummaryTrait
103
+ clean_record: CleanRecordTrait
104
+ sound_volume: SoundVolumeTrait
105
+ rooms: RoomsTrait
106
+ maps: MapsTrait
107
+ map_content: MapContentTrait
108
+ consumables: ConsumableTrait
109
+ home: HomeTrait
110
+ device_features: DeviceFeaturesTrait
111
+
112
+ # Optional features that may not be supported on all devices
113
+ child_lock: ChildLockTrait | None = None
114
+ led_status: LedStatusTrait | None = None
115
+ flow_led_status: FlowLedStatusTrait | None = None
116
+ valley_electricity_timer: ValleyElectricityTimerTrait | None = None
117
+ dust_collection_mode: DustCollectionModeTrait | None = None
118
+ wash_towel_mode: WashTowelModeTrait | None = None
119
+ smart_wash_params: SmartWashParamsTrait | None = None
120
+
121
+ def __init__(
122
+ self,
123
+ product: HomeDataProduct,
124
+ home_data: HomeData,
125
+ rpc_channel: V1RpcChannel,
126
+ mqtt_rpc_channel: V1RpcChannel,
127
+ map_rpc_channel: V1RpcChannel,
128
+ cache: Cache,
129
+ map_parser_config: MapParserConfig | None = None,
130
+ ) -> None:
131
+ """Initialize the V1TraitProps."""
132
+ self._rpc_channel = rpc_channel
133
+ self._mqtt_rpc_channel = mqtt_rpc_channel
134
+ self._map_rpc_channel = map_rpc_channel
135
+ self._cache = cache
136
+
137
+ self.status = StatusTrait(product)
138
+ self.clean_summary = CleanSummaryTrait()
139
+ self.clean_record = CleanRecordTrait(self.clean_summary)
140
+ self.consumables = ConsumableTrait()
141
+ self.rooms = RoomsTrait(home_data)
142
+ self.maps = MapsTrait(self.status)
143
+ self.map_content = MapContentTrait(map_parser_config)
144
+ self.home = HomeTrait(self.status, self.maps, self.rooms, cache)
145
+ self.device_features = DeviceFeaturesTrait(product.product_nickname, cache)
146
+
147
+ # Dynamically create any traits that need to be populated
148
+ for item in fields(self):
149
+ if (trait := getattr(self, item.name, None)) is None:
150
+ # We exclude optional features and them via discover_features
151
+ if (union_args := get_args(item.type)) is None or len(union_args) > 0:
152
+ continue
153
+ _LOGGER.debug("Trait '%s' is supported, initializing", item.name)
154
+ trait = item.type()
155
+ setattr(self, item.name, trait)
156
+ # This is a hack to allow setting the rpc_channel on all traits. This is
157
+ # used so we can preserve the dataclass behavior when the values in the
158
+ # traits are updated, but still want to allow them to have a reference
159
+ # to the rpc channel for sending commands.
160
+ trait._rpc_channel = self._get_rpc_channel(trait)
161
+
162
+ def _get_rpc_channel(self, trait: V1TraitMixin) -> V1RpcChannel:
163
+ # The decorator `@common.mqtt_rpc_channel` means that the trait needs
164
+ # to use the mqtt_rpc_channel (cloud only) instead of the rpc_channel (adaptive)
165
+ if hasattr(trait, "mqtt_rpc_channel"):
166
+ return self._mqtt_rpc_channel
167
+ elif hasattr(trait, "map_rpc_channel"):
168
+ return self._map_rpc_channel
169
+ else:
170
+ return self._rpc_channel
171
+
172
+ async def discover_features(self) -> None:
173
+ """Populate any supported traits that were not initialized in __init__."""
174
+ _LOGGER.debug("Starting optional trait discovery")
175
+ await self.device_features.refresh()
176
+ # Dock type also acts like a device feature for some traits.
177
+ dock_type = await self._dock_type()
178
+
179
+ # Dynamically create any traits that need to be populated
180
+ for item in fields(self):
181
+ if (trait := getattr(self, item.name, None)) is not None:
182
+ continue
183
+ if (union_args := get_args(item.type)) is None:
184
+ raise ValueError(f"Unexpected non-union type for trait {item.name}: {item.type}")
185
+ if len(union_args) != 2 or type(None) not in union_args:
186
+ raise ValueError(f"Unexpected non-optional type for trait {item.name}: {item.type}")
187
+
188
+ # Union args may not be in declared order
189
+ item_type = union_args[0] if union_args[1] is type(None) else union_args[1]
190
+ if not self._is_supported(item_type, item.name, dock_type):
191
+ _LOGGER.debug("Trait '%s' not supported, skipping", item.name)
192
+ continue
193
+ _LOGGER.debug("Trait '%s' is supported, initializing", item.name)
194
+ trait = item_type()
195
+ setattr(self, item.name, trait)
196
+ trait._rpc_channel = self._get_rpc_channel(trait)
197
+
198
+ def _is_supported(self, trait_type: type[V1TraitMixin], name: str, dock_type: RoborockDockTypeCode) -> bool:
199
+ """Check if a trait is supported by the device."""
200
+
201
+ if (requires_dock_type := getattr(trait_type, "requires_dock_type", None)) is not None:
202
+ return requires_dock_type(dock_type)
203
+
204
+ if (feature_name := getattr(trait_type, "requires_feature", None)) is None:
205
+ _LOGGER.debug("Optional trait missing 'requires_feature' attribute %s, skipping", name)
206
+ return False
207
+ if (is_supported := getattr(self.device_features, feature_name)) is None:
208
+ raise ValueError(f"Device feature '{feature_name}' on trait '{name}' is unknown")
209
+ return is_supported
210
+
211
+ async def _dock_type(self) -> RoborockDockTypeCode:
212
+ """Get the dock type from the status trait or cache."""
213
+ dock_type = await self._get_cached_trait_data("dock_type")
214
+ if dock_type is not None:
215
+ _LOGGER.debug("Using cached dock type: %s", dock_type)
216
+ try:
217
+ return RoborockDockTypeCode(dock_type)
218
+ except ValueError:
219
+ _LOGGER.debug("Cached dock type %s is invalid, refreshing", dock_type)
220
+
221
+ _LOGGER.debug("Starting dock type discovery")
222
+ await self.status.refresh()
223
+ _LOGGER.debug("Fetched dock type: %s", self.status.dock_type)
224
+ if self.status.dock_type is None:
225
+ # Explicitly set so we reuse cached value next type
226
+ dock_type = RoborockDockTypeCode.no_dock
227
+ else:
228
+ dock_type = self.status.dock_type
229
+ await self._set_cached_trait_data("dock_type", dock_type)
230
+ return dock_type
231
+
232
+ async def _get_cached_trait_data(self, name: str) -> Any:
233
+ """Get the dock type from the status trait or cache."""
234
+ cache_data = await self._cache.get()
235
+ if cache_data.trait_data is None:
236
+ cache_data.trait_data = {}
237
+ _LOGGER.debug("Cached trait data: %s", cache_data.trait_data)
238
+ return cache_data.trait_data.get(name)
239
+
240
+ async def _set_cached_trait_data(self, name: str, value: Any) -> None:
241
+ """Set trait-specific cached data."""
242
+ cache_data = await self._cache.get()
243
+ if cache_data.trait_data is None:
244
+ cache_data.trait_data = {}
245
+ cache_data.trait_data[name] = value
246
+ _LOGGER.debug("Updating cached trait data: %s", cache_data.trait_data)
247
+ await self._cache.set(cache_data)
248
+
249
+
250
+ def create(
251
+ product: HomeDataProduct,
252
+ home_data: HomeData,
253
+ rpc_channel: V1RpcChannel,
254
+ mqtt_rpc_channel: V1RpcChannel,
255
+ map_rpc_channel: V1RpcChannel,
256
+ cache: Cache,
257
+ map_parser_config: MapParserConfig | None = None,
258
+ ) -> PropertiesApi:
259
+ """Create traits for V1 devices."""
260
+ return PropertiesApi(product, home_data, rpc_channel, mqtt_rpc_channel, map_rpc_channel, cache, map_parser_config)
@@ -5,12 +5,17 @@ from roborock.roborock_typing import RoborockCommand
5
5
  _STATUS_PARAM = "lock_status"
6
6
 
7
7
 
8
- class ChildLockTrait(ChildLockStatus, common.V1TraitMixin):
8
+ class ChildLockTrait(ChildLockStatus, common.V1TraitMixin, common.RoborockSwitchBase):
9
9
  """Trait for controlling the child lock of a Roborock device."""
10
10
 
11
11
  command = RoborockCommand.GET_CHILD_LOCK_STATUS
12
12
  requires_feature = "is_set_child_supported"
13
13
 
14
+ @property
15
+ def is_on(self) -> bool:
16
+ """Return whether the child lock is enabled."""
17
+ return self.lock_status == 1
18
+
14
19
  async def enable(self) -> None:
15
20
  """Enable the child lock."""
16
21
  await self.rpc_channel.send_command(RoborockCommand.SET_CHILD_LOCK_STATUS, params={_STATUS_PARAM: 1})
@@ -0,0 +1,69 @@
1
+ """Trait for getting the last clean record."""
2
+
3
+ import logging
4
+ from typing import Self
5
+
6
+ from roborock.containers import CleanRecord
7
+ from roborock.devices.traits.v1 import common
8
+ from roborock.roborock_typing import RoborockCommand
9
+ from roborock.util import unpack_list
10
+
11
+ from .clean_summary import CleanSummaryTrait
12
+
13
+ _LOGGER = logging.getLogger(__name__)
14
+
15
+
16
+ class CleanRecordTrait(CleanRecord, common.V1TraitMixin):
17
+ """Trait for getting the last clean record."""
18
+
19
+ command = RoborockCommand.GET_CLEAN_RECORD
20
+
21
+ def __init__(self, clean_summary_trait: CleanSummaryTrait) -> None:
22
+ """Initialize the clean record trait."""
23
+ super().__init__()
24
+ self._clean_summary_trait = clean_summary_trait
25
+
26
+ async def refresh(self) -> Self:
27
+ """Get the last clean record.
28
+
29
+ Assumes that the clean summary has already been fetched.
30
+ """
31
+ if not self._clean_summary_trait.records:
32
+ _LOGGER.debug("No clean records available in clean summary.")
33
+ return self
34
+ last_record_id = self._clean_summary_trait.records[-1]
35
+ response = await self.rpc_channel.send_command(self.command, params=[last_record_id])
36
+ new_self = self._parse_response(response)
37
+ self._update_trait_values(new_self)
38
+ return self
39
+
40
+ @classmethod
41
+ def _parse_type_response(cls, response: common.V1ResponseData) -> CleanRecord:
42
+ """Parse the response from the device into a CleanRecord."""
43
+ if isinstance(response, dict):
44
+ return CleanRecord.from_dict(response)
45
+ if isinstance(response, list):
46
+ if isinstance(response[-1], dict):
47
+ records = [CleanRecord.from_dict(rec) for rec in response]
48
+ final_record = records[-1]
49
+ try:
50
+ # This code is semi-presumptions - so it is put in a try finally to be safe.
51
+ final_record.begin = records[0].begin
52
+ final_record.begin_datetime = records[0].begin_datetime
53
+ final_record.start_type = records[0].start_type
54
+ for rec in records[0:-1]:
55
+ final_record.duration = (final_record.duration or 0) + (rec.duration or 0)
56
+ final_record.area = (final_record.area or 0) + (rec.area or 0)
57
+ final_record.avoid_count = (final_record.avoid_count or 0) + (rec.avoid_count or 0)
58
+ final_record.wash_count = (final_record.wash_count or 0) + (rec.wash_count or 0)
59
+ final_record.square_meter_area = (final_record.square_meter_area or 0) + (
60
+ rec.square_meter_area or 0
61
+ )
62
+ return final_record
63
+ except Exception:
64
+ # Return final record when an exception occurred
65
+ return final_record
66
+ # There are still a few unknown variables in this.
67
+ begin, end, duration, area = unpack_list(response, 4)
68
+ return CleanRecord(begin=begin, end=end, duration=duration, area=area)
69
+ raise ValueError(f"Unexpected clean record format: {response!r}")
@@ -4,7 +4,7 @@ This is an internal library and should not be used directly by consumers.
4
4
  """
5
5
 
6
6
  import logging
7
- from abc import ABC
7
+ from abc import ABC, abstractmethod
8
8
  from dataclasses import dataclass, fields
9
9
  from typing import ClassVar, Self
10
10
 
@@ -82,6 +82,7 @@ class V1TraitMixin(ABC):
82
82
  new_data = self._parse_response(response)
83
83
  if not isinstance(new_data, RoborockBase):
84
84
  raise ValueError(f"Internal error, unexpected response type: {new_data!r}")
85
+ _LOGGER.debug("Refreshed %s: %s", self.__class__.__name__, new_data)
85
86
  self._update_trait_values(new_data)
86
87
  return self
87
88
 
@@ -124,6 +125,23 @@ class RoborockValueBase(V1TraitMixin, RoborockBase):
124
125
  return cls(**{value_field: response})
125
126
 
126
127
 
128
+ class RoborockSwitchBase(ABC):
129
+ """Base class for traits that represent a boolean switch."""
130
+
131
+ @property
132
+ @abstractmethod
133
+ def is_on(self) -> bool:
134
+ """Return whether the switch is on."""
135
+
136
+ @abstractmethod
137
+ async def enable(self) -> None:
138
+ """Enable the switch."""
139
+
140
+ @abstractmethod
141
+ async def disable(self) -> None:
142
+ """Disable the switch."""
143
+
144
+
127
145
  def mqtt_rpc_channel(cls):
128
146
  """Decorator to mark a function as cloud only.
129
147
 
@@ -5,11 +5,16 @@ from roborock.roborock_typing import RoborockCommand
5
5
  _ENABLED_PARAM = "enabled"
6
6
 
7
7
 
8
- class DoNotDisturbTrait(DnDTimer, common.V1TraitMixin):
8
+ 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
12
 
13
+ @property
14
+ def is_on(self) -> bool:
15
+ """Return whether the Do Not Disturb (DND) timer is enabled."""
16
+ return self.enabled == 1
17
+
13
18
  async def set_dnd_timer(self, dnd_timer: DnDTimer) -> None:
14
19
  """Set the Do Not Disturb (DND) timer settings of the device."""
15
20
  await self.rpc_channel.send_command(RoborockCommand.SET_DND_TIMER, params=dnd_timer.as_dict())
@@ -0,0 +1,13 @@
1
+ """Trait for dust collection mode."""
2
+
3
+ from roborock.containers import DustCollectionMode
4
+ from roborock.device_features import is_valid_dock
5
+ from roborock.devices.traits.v1 import common
6
+ from roborock.roborock_typing import RoborockCommand
7
+
8
+
9
+ class DustCollectionModeTrait(DustCollectionMode, common.V1TraitMixin):
10
+ """Trait for dust collection mode."""
11
+
12
+ command = RoborockCommand.GET_DUST_COLLECTION_MODE
13
+ requires_dock_type = is_valid_dock
@@ -5,12 +5,17 @@ from roborock.roborock_typing import RoborockCommand
5
5
  _STATUS_PARAM = "status"
6
6
 
7
7
 
8
- class FlowLedStatusTrait(FlowLedStatus, common.V1TraitMixin):
8
+ class FlowLedStatusTrait(FlowLedStatus, common.V1TraitMixin, common.RoborockSwitchBase):
9
9
  """Trait for controlling the Flow LED status of a Roborock device."""
10
10
 
11
11
  command = RoborockCommand.GET_FLOW_LED_STATUS
12
12
  requires_feature = "is_flow_led_setting_supported"
13
13
 
14
+ @property
15
+ def is_on(self) -> bool:
16
+ """Return whether the Flow LED status is enabled."""
17
+ return self.status == 1
18
+
14
19
  async def enable(self) -> None:
15
20
  """Enable the Flow LED status."""
16
21
  await self.rpc_channel.send_command(RoborockCommand.SET_FLOW_LED_STATUS, params={_STATUS_PARAM: 1})
@@ -5,12 +5,17 @@ from roborock.roborock_typing import RoborockCommand
5
5
  from .common import V1ResponseData
6
6
 
7
7
 
8
- class LedStatusTrait(LedStatus, common.V1TraitMixin):
8
+ class LedStatusTrait(LedStatus, common.V1TraitMixin, common.RoborockSwitchBase):
9
9
  """Trait for controlling the LED status of a Roborock device."""
10
10
 
11
11
  command = RoborockCommand.GET_LED_STATUS
12
12
  requires_feature = "is_led_status_switch_supported"
13
13
 
14
+ @property
15
+ def is_on(self) -> bool:
16
+ """Return whether the LED status is enabled."""
17
+ return self.status == 1
18
+
14
19
  async def enable(self) -> None:
15
20
  """Enable the LED status."""
16
21
  await self.rpc_channel.send_command(RoborockCommand.SET_LED_STATUS, params=[1])
@@ -0,0 +1,13 @@
1
+ """Trait for smart wash parameters."""
2
+
3
+ from roborock.containers import SmartWashParams
4
+ from roborock.device_features import is_wash_n_fill_dock
5
+ from roborock.devices.traits.v1 import common
6
+ from roborock.roborock_typing import RoborockCommand
7
+
8
+
9
+ class SmartWashParamsTrait(SmartWashParams, common.V1TraitMixin):
10
+ """Trait for smart wash parameters."""
11
+
12
+ command = RoborockCommand.GET_SMART_WASH_PARAMS
13
+ requires_dock_type = is_wash_n_fill_dock
@@ -11,6 +11,11 @@ class ValleyElectricityTimerTrait(ValleyElectricityTimer, common.V1TraitMixin):
11
11
  command = RoborockCommand.GET_VALLEY_ELECTRICITY_TIMER
12
12
  requires_feature = "is_supported_valley_electricity"
13
13
 
14
+ @property
15
+ def is_on(self) -> bool:
16
+ """Return whether the Valley Electricity Timer is enabled."""
17
+ return self.enabled == 1
18
+
14
19
  async def set_timer(self, timer: ValleyElectricityTimer) -> None:
15
20
  """Set the Valley Electricity Timer settings of the device."""
16
21
  await self.rpc_channel.send_command(RoborockCommand.SET_VALLEY_ELECTRICITY_TIMER, params=timer.as_dict())
@@ -0,0 +1,13 @@
1
+ """Trait for wash towel mode."""
2
+
3
+ from roborock.containers import WashTowelMode
4
+ from roborock.device_features import is_wash_n_fill_dock
5
+ from roborock.devices.traits.v1 import common
6
+ from roborock.roborock_typing import RoborockCommand
7
+
8
+
9
+ class WashTowelModeTrait(WashTowelMode, common.V1TraitMixin):
10
+ """Trait for wash towel mode."""
11
+
12
+ command = RoborockCommand.GET_WASH_TOWEL_MODE
13
+ requires_dock_type = is_wash_n_fill_dock
@@ -123,10 +123,18 @@ class V1Channel(Channel):
123
123
  async def subscribe(self, callback: Callable[[RoborockMessage], None]) -> Callable[[], None]:
124
124
  """Subscribe to all messages from the device.
125
125
 
126
- This will establish MQTT connection first, and also attempt to set up
127
- local connection if possible. Any failures to subscribe to MQTT will raise
128
- a RoborockException. A local connection failure will not raise an exception,
129
- since the local connection is optional.
126
+ This will first attempt to establish a local connection to the device
127
+ using cached network information if available. If that fails, it will
128
+ fall back to using the MQTT connection.
129
+
130
+ A background task will be started to monitor and maintain the local
131
+ connection, attempting to reconnect as needed.
132
+
133
+ Args:
134
+ callback: Callback to invoke for each received message.
135
+
136
+ Returns:
137
+ Unsubscribe function to stop receiving messages and clean up resources.
130
138
  """
131
139
  if self._callback is not None:
132
140
  raise ValueError("Only one subscription allowed at a time")
@@ -45,6 +45,7 @@ from roborock.containers import (
45
45
  ValleyElectricityTimer,
46
46
  WashTowelMode,
47
47
  )
48
+ from roborock.device_features import WASH_N_FILL_DOCK_TYPES
48
49
  from roborock.protocols.v1_protocol import MapResponse, SecurityData, create_map_response_decoder
49
50
  from roborock.roborock_message import (
50
51
  ROBOROCK_DATA_CONSUMABLE_PROTOCOL,
@@ -64,16 +65,6 @@ COMMANDS_SECURED = {
64
65
 
65
66
  CLOUD_REQUIRED = COMMANDS_SECURED.union(CUSTOM_COMMANDS)
66
67
 
67
- WASH_N_FILL_DOCK = [
68
- RoborockDockTypeCode.empty_wash_fill_dock,
69
- RoborockDockTypeCode.s8_dock,
70
- RoborockDockTypeCode.p10_dock,
71
- RoborockDockTypeCode.p10_pro_dock,
72
- RoborockDockTypeCode.s8_maxv_ultra_dock,
73
- RoborockDockTypeCode.qrevo_s_dock,
74
- RoborockDockTypeCode.saros_r10_dock,
75
- RoborockDockTypeCode.qrevo_curv_dock,
76
- ]
77
68
  RT = TypeVar("RT", bound=RoborockBase)
78
69
  EVICT_TIME = 60
79
70
 
@@ -264,7 +255,7 @@ class RoborockClientV1(RoborockClient, ABC):
264
255
  DustCollectionMode | WashTowelMode | SmartWashParams | None,
265
256
  ]
266
257
  ] = [self.get_dust_collection_mode()]
267
- if dock_type in WASH_N_FILL_DOCK:
258
+ if dock_type in WASH_N_FILL_DOCK_TYPES:
268
259
  commands += [
269
260
  self.get_wash_towel_mode(),
270
261
  self.get_smart_wash_params(),
@@ -1,163 +0,0 @@
1
- """Create traits for V1 devices."""
2
-
3
- import logging
4
- from dataclasses import dataclass, field, fields
5
- from typing import get_args
6
-
7
- from roborock.containers import HomeData, HomeDataProduct
8
- from roborock.devices.cache import Cache
9
- from roborock.devices.traits import Trait
10
- from roborock.devices.v1_rpc_channel import V1RpcChannel
11
- from roborock.map.map_parser import MapParserConfig
12
-
13
- from .child_lock import ChildLockTrait
14
- from .clean_summary import CleanSummaryTrait
15
- from .command import CommandTrait
16
- from .common import V1TraitMixin
17
- from .consumeable import ConsumableTrait
18
- from .device_features import DeviceFeaturesTrait
19
- from .do_not_disturb import DoNotDisturbTrait
20
- from .flow_led_status import FlowLedStatusTrait
21
- from .home import HomeTrait
22
- from .led_status import LedStatusTrait
23
- from .map_content import MapContentTrait
24
- from .maps import MapsTrait
25
- from .rooms import RoomsTrait
26
- from .status import StatusTrait
27
- from .volume import SoundVolumeTrait
28
-
29
- _LOGGER = logging.getLogger(__name__)
30
-
31
- __all__ = [
32
- "create",
33
- "PropertiesApi",
34
- "StatusTrait",
35
- "DoNotDisturbTrait",
36
- "CleanSummaryTrait",
37
- "SoundVolumeTrait",
38
- "MapsTrait",
39
- "MapContentTrait",
40
- "ConsumableTrait",
41
- "HomeTrait",
42
- "DeviceFeaturesTrait",
43
- "CommandTrait",
44
- "ChildLockTrait",
45
- "FlowLedStatusTrait",
46
- "LedStatusTrait",
47
- ]
48
-
49
-
50
- @dataclass
51
- class PropertiesApi(Trait):
52
- """Common properties for V1 devices.
53
-
54
- This class holds all the traits that are common across all V1 devices.
55
- """
56
-
57
- # All v1 devices have these traits
58
- status: StatusTrait
59
- command: CommandTrait
60
- dnd: DoNotDisturbTrait
61
- clean_summary: CleanSummaryTrait
62
- sound_volume: SoundVolumeTrait
63
- rooms: RoomsTrait
64
- maps: MapsTrait
65
- map_content: MapContentTrait
66
- consumables: ConsumableTrait
67
- home: HomeTrait
68
- device_features: DeviceFeaturesTrait
69
-
70
- # Optional features that may not be supported on all devices
71
- child_lock: ChildLockTrait | None = None
72
- led_status: LedStatusTrait | None = None
73
- flow_led_status: FlowLedStatusTrait | None = None
74
-
75
- def __init__(
76
- self,
77
- product: HomeDataProduct,
78
- home_data: HomeData,
79
- rpc_channel: V1RpcChannel,
80
- mqtt_rpc_channel: V1RpcChannel,
81
- map_rpc_channel: V1RpcChannel,
82
- cache: Cache,
83
- map_parser_config: MapParserConfig | None = None,
84
- ) -> None:
85
- """Initialize the V1TraitProps."""
86
- self._rpc_channel = rpc_channel
87
- self._mqtt_rpc_channel = mqtt_rpc_channel
88
- self._map_rpc_channel = map_rpc_channel
89
-
90
- self.status = StatusTrait(product)
91
- self.rooms = RoomsTrait(home_data)
92
- self.maps = MapsTrait(self.status)
93
- self.map_content = MapContentTrait(map_parser_config)
94
- self.home = HomeTrait(self.status, self.maps, self.rooms, cache)
95
- self.device_features = DeviceFeaturesTrait(product.product_nickname, cache)
96
-
97
- # Dynamically create any traits that need to be populated
98
- for item in fields(self):
99
- if (trait := getattr(self, item.name, None)) is None:
100
- # We exclude optional features and them via discover_features
101
- if (union_args := get_args(item.type)) is None or len(union_args) > 0:
102
- continue
103
- _LOGGER.debug("Initializing trait %s", item.name)
104
- trait = item.type()
105
- setattr(self, item.name, trait)
106
- # This is a hack to allow setting the rpc_channel on all traits. This is
107
- # used so we can preserve the dataclass behavior when the values in the
108
- # traits are updated, but still want to allow them to have a reference
109
- # to the rpc channel for sending commands.
110
- trait._rpc_channel = self._get_rpc_channel(trait)
111
-
112
- def _get_rpc_channel(self, trait: V1TraitMixin) -> V1RpcChannel:
113
- # The decorator `@common.mqtt_rpc_channel` means that the trait needs
114
- # to use the mqtt_rpc_channel (cloud only) instead of the rpc_channel (adaptive)
115
- if hasattr(trait, "mqtt_rpc_channel"):
116
- return self._mqtt_rpc_channel
117
- elif hasattr(trait, "map_rpc_channel"):
118
- return self._map_rpc_channel
119
- else:
120
- return self._rpc_channel
121
-
122
- async def discover_features(self) -> None:
123
- """Populate any supported traits that were not initialized in __init__."""
124
- await self.device_features.refresh()
125
-
126
- for item in fields(self):
127
- if (trait := getattr(self, item.name, None)) is not None:
128
- continue
129
- if (union_args := get_args(item.type)) is None:
130
- raise ValueError(f"Unexpected non-union type for trait {item.name}: {item.type}")
131
- if len(union_args) != 2 or type(None) not in union_args:
132
- raise ValueError(f"Unexpected non-optional type for trait {item.name}: {item.type}")
133
-
134
- # Union args may not be in declared order
135
- item_type = union_args[0] if union_args[1] is type(None) else union_args[1]
136
- trait = item_type()
137
- if not hasattr(trait, "requires_feature"):
138
- _LOGGER.debug("Trait missing required feature %s", item.name)
139
- continue
140
- _LOGGER.debug("Checking for feature %s", trait.requires_feature)
141
- is_supported = getattr(self.device_features, trait.requires_feature)
142
- # _LOGGER.debug("Device features: %s", self.device_features)
143
- if is_supported is None:
144
- raise ValueError(f"Device feature '{trait.requires_feature}' on trait '{item.name}' is unknown")
145
- if not is_supported:
146
- _LOGGER.debug("Disabling optional feature trait %s", item.name)
147
- continue
148
- _LOGGER.debug("Enabling optional feature trait %s", item.name)
149
- setattr(self, item.name, trait)
150
- trait._rpc_channel = self._get_rpc_channel(trait)
151
-
152
-
153
- def create(
154
- product: HomeDataProduct,
155
- home_data: HomeData,
156
- rpc_channel: V1RpcChannel,
157
- mqtt_rpc_channel: V1RpcChannel,
158
- map_rpc_channel: V1RpcChannel,
159
- cache: Cache,
160
- map_parser_config: MapParserConfig | None = None,
161
- ) -> PropertiesApi:
162
- """Create traits for V1 devices."""
163
- return PropertiesApi(product, home_data, rpc_channel, mqtt_rpc_channel, map_rpc_channel, cache, map_parser_config)