python-roborock 3.7.2__tar.gz → 3.8.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 (97) hide show
  1. {python_roborock-3.7.2 → python_roborock-3.8.0}/PKG-INFO +25 -26
  2. {python_roborock-3.7.2 → python_roborock-3.8.0}/README.md +24 -25
  3. {python_roborock-3.7.2 → python_roborock-3.8.0}/pyproject.toml +2 -2
  4. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/__init__.py +5 -6
  5. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/data/containers.py +3 -0
  6. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/README.md +5 -3
  7. python_roborock-3.8.0/roborock/devices/__init__.py +11 -0
  8. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/device.py +47 -0
  9. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/device_manager.py +1 -1
  10. python_roborock-3.8.0/roborock/devices/file_cache.py +70 -0
  11. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/b01/__init__.py +1 -2
  12. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/v1/__init__.py +20 -20
  13. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/v1/clean_summary.py +2 -3
  14. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/v1/common.py +1 -2
  15. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/v1/device_features.py +5 -7
  16. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/v1/do_not_disturb.py +4 -13
  17. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/v1/home.py +2 -3
  18. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/v1/network_info.py +3 -6
  19. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/v1/valley_electricity_timer.py +3 -10
  20. python_roborock-3.8.0/roborock/protocols/__init__.py +3 -0
  21. python_roborock-3.7.2/roborock/devices/__init__.py +0 -7
  22. {python_roborock-3.7.2 → python_roborock-3.8.0}/.gitignore +0 -0
  23. {python_roborock-3.7.2 → python_roborock-3.8.0}/LICENSE +0 -0
  24. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/api.py +0 -0
  25. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/broadcast_protocol.py +0 -0
  26. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/callbacks.py +0 -0
  27. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/cli.py +0 -0
  28. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/cloud_api.py +0 -0
  29. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/command_cache.py +0 -0
  30. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/const.py +0 -0
  31. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/data/__init__.py +0 -0
  32. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/data/b01_q10/__init__.py +0 -0
  33. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
  34. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
  35. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/data/b01_q7/__init__.py +0 -0
  36. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
  37. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
  38. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/data/code_mappings.py +0 -0
  39. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/data/dyad/__init__.py +0 -0
  40. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  41. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/data/dyad/dyad_containers.py +0 -0
  42. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/data/v1/__init__.py +0 -0
  43. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/data/v1/v1_clean_modes.py +0 -0
  44. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/data/v1/v1_code_mappings.py +0 -0
  45. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/data/v1/v1_containers.py +0 -0
  46. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/data/zeo/__init__.py +0 -0
  47. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
  48. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/data/zeo/zeo_containers.py +0 -0
  49. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/device_features.py +0 -0
  50. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/a01_channel.py +0 -0
  51. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/b01_channel.py +0 -0
  52. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/cache.py +0 -0
  53. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/channel.py +0 -0
  54. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/local_channel.py +0 -0
  55. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/mqtt_channel.py +0 -0
  56. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/__init__.py +0 -0
  57. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/a01/__init__.py +0 -0
  58. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/traits_mixin.py +0 -0
  59. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/v1/child_lock.py +0 -0
  60. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/v1/command.py +0 -0
  61. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/v1/consumeable.py +0 -0
  62. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
  63. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
  64. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/v1/led_status.py +0 -0
  65. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/v1/map_content.py +0 -0
  66. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/v1/maps.py +0 -0
  67. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/v1/rooms.py +0 -0
  68. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/v1/routines.py +0 -0
  69. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
  70. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/v1/status.py +0 -0
  71. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/v1/volume.py +0 -0
  72. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
  73. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/v1_channel.py +0 -0
  74. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/devices/v1_rpc_channel.py +0 -0
  75. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/exceptions.py +0 -0
  76. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/map/__init__.py +0 -0
  77. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/map/map_parser.py +0 -0
  78. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/mqtt/__init__.py +0 -0
  79. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/mqtt/roborock_session.py +0 -0
  80. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/mqtt/session.py +0 -0
  81. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/protocol.py +0 -0
  82. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/protocols/a01_protocol.py +0 -0
  83. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/protocols/b01_protocol.py +0 -0
  84. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/protocols/v1_protocol.py +0 -0
  85. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/py.typed +0 -0
  86. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/roborock_future.py +0 -0
  87. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/roborock_message.py +0 -0
  88. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/roborock_typing.py +0 -0
  89. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/util.py +0 -0
  90. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/version_1_apis/__init__.py +0 -0
  91. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
  92. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
  93. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  94. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/version_a01_apis/__init__.py +0 -0
  95. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  96. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
  97. {python_roborock-3.7.2 → python_roborock-3.8.0}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-roborock
3
- Version: 3.7.2
3
+ Version: 3.8.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/
@@ -49,16 +49,14 @@ Install this via pip (or your favourite package manager):
49
49
 
50
50
  You can see all of the commands supported [here](https://python-roborock.readthedocs.io/en/latest/api_commands.html)
51
51
 
52
- ## Sending Commands
52
+ ## Example Usage
53
53
 
54
- Here is an example that requires no manual intervention and can be done all automatically. You can skip some steps by
55
- caching values or looking at them and grabbing them manually.
56
54
  ```python
57
55
  import asyncio
58
56
 
59
- from roborock import HomeDataProduct, DeviceData, RoborockCommand
60
- from roborock.version_1_apis import RoborockMqttClientV1, RoborockLocalClientV1
61
57
  from roborock.web_api import RoborockApiClient
58
+ from roborock.devices.device_manager import create_device_manager, UserParams
59
+
62
60
 
63
61
  async def main():
64
62
  web_api = RoborockApiClient(username="youremailhere")
@@ -69,30 +67,31 @@ async def main():
69
67
  code = input("What is the code?")
70
68
  user_data = await web_api.code_login(code)
71
69
 
72
- # Get home data
73
- home_data = await web_api.get_home_data_v2(user_data)
74
-
75
- # Get the device you want
76
- device = home_data.devices[0]
77
-
78
- # Get product ids:
79
- product_info: dict[str, HomeDataProduct] = {
80
- product.id: product for product in home_data.products
81
- }
82
- # Create the Mqtt(aka cloud required) Client
83
- device_data = DeviceData(device, product_info[device.product_id].model)
84
- mqtt_client = RoborockMqttClientV1(user_data, device_data)
85
- networking = await mqtt_client.get_networking()
86
- local_device_data = DeviceData(device, product_info[device.product_id].model, networking.ip)
87
- local_client = RoborockLocalClientV1(local_device_data)
88
- # You can use the send_command to send any command to the device
89
- status = await local_client.send_command(RoborockCommand.GET_STATUS)
90
- # Or use existing functions that will give you data classes
91
- status = await local_client.get_status()
70
+ # Create a device manager that can discover devices.
71
+ user_params = UserParams(
72
+ username="youremailhere",
73
+ user_data=user_data,
74
+ )
75
+ device_manager = await create_device_manager(user_params)
76
+ devices = await device_manager.get_devices()
77
+
78
+ # Get all vacuum devices that support the v1 PropertiesApi
79
+ for device in devices:
80
+ if not device.v1_properties:
81
+ continue
82
+
83
+ # Refresh the current device status
84
+ status_trait = device.v1_properties.status
85
+ await status_trait.refresh()
86
+ print(status_trait)
92
87
 
93
88
  asyncio.run(main())
94
89
  ```
95
90
 
91
+ See [examples/example.py](examples/example.py) for a more full featured example
92
+ that has performance improvements to cache cloud information to prefer
93
+ connections over the local network.
94
+
96
95
  ## Supported devices
97
96
 
98
97
  You can find what devices are supported
@@ -20,16 +20,14 @@ Install this via pip (or your favourite package manager):
20
20
 
21
21
  You can see all of the commands supported [here](https://python-roborock.readthedocs.io/en/latest/api_commands.html)
22
22
 
23
- ## Sending Commands
23
+ ## Example Usage
24
24
 
25
- Here is an example that requires no manual intervention and can be done all automatically. You can skip some steps by
26
- caching values or looking at them and grabbing them manually.
27
25
  ```python
28
26
  import asyncio
29
27
 
30
- from roborock import HomeDataProduct, DeviceData, RoborockCommand
31
- from roborock.version_1_apis import RoborockMqttClientV1, RoborockLocalClientV1
32
28
  from roborock.web_api import RoborockApiClient
29
+ from roborock.devices.device_manager import create_device_manager, UserParams
30
+
33
31
 
34
32
  async def main():
35
33
  web_api = RoborockApiClient(username="youremailhere")
@@ -40,30 +38,31 @@ async def main():
40
38
  code = input("What is the code?")
41
39
  user_data = await web_api.code_login(code)
42
40
 
43
- # Get home data
44
- home_data = await web_api.get_home_data_v2(user_data)
45
-
46
- # Get the device you want
47
- device = home_data.devices[0]
48
-
49
- # Get product ids:
50
- product_info: dict[str, HomeDataProduct] = {
51
- product.id: product for product in home_data.products
52
- }
53
- # Create the Mqtt(aka cloud required) Client
54
- device_data = DeviceData(device, product_info[device.product_id].model)
55
- mqtt_client = RoborockMqttClientV1(user_data, device_data)
56
- networking = await mqtt_client.get_networking()
57
- local_device_data = DeviceData(device, product_info[device.product_id].model, networking.ip)
58
- local_client = RoborockLocalClientV1(local_device_data)
59
- # You can use the send_command to send any command to the device
60
- status = await local_client.send_command(RoborockCommand.GET_STATUS)
61
- # Or use existing functions that will give you data classes
62
- status = await local_client.get_status()
41
+ # Create a device manager that can discover devices.
42
+ user_params = UserParams(
43
+ username="youremailhere",
44
+ user_data=user_data,
45
+ )
46
+ device_manager = await create_device_manager(user_params)
47
+ devices = await device_manager.get_devices()
48
+
49
+ # Get all vacuum devices that support the v1 PropertiesApi
50
+ for device in devices:
51
+ if not device.v1_properties:
52
+ continue
53
+
54
+ # Refresh the current device status
55
+ status_trait = device.v1_properties.status
56
+ await status_trait.refresh()
57
+ print(status_trait)
63
58
 
64
59
  asyncio.run(main())
65
60
  ```
66
61
 
62
+ See [examples/example.py](examples/example.py) for a more full featured example
63
+ that has performance improvements to cache cloud information to prefer
64
+ connections over the local network.
65
+
67
66
  ## Supported devices
68
67
 
69
68
  You can find what devices are supported
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-roborock"
3
- version = "3.7.2"
3
+ version = "3.8.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"
@@ -44,7 +44,7 @@ dev = [
44
44
  "pytest",
45
45
  "pre-commit>=3.5,<5.0",
46
46
  "mypy",
47
- "ruff==0.14.1",
47
+ "ruff==0.14.4",
48
48
  "codespell",
49
49
  "pyshark>=0.6,<0.7",
50
50
  "aioresponses>=0.7.7,<0.8",
@@ -11,6 +11,7 @@ from . import (
11
11
  cloud_api,
12
12
  const,
13
13
  data,
14
+ devices,
14
15
  exceptions,
15
16
  roborock_typing,
16
17
  version_1_apis,
@@ -19,13 +20,11 @@ from . import (
19
20
  )
20
21
 
21
22
  __all__ = [
23
+ "devices",
24
+ "data",
25
+ "map",
22
26
  "web_api",
23
- "version_1_apis",
24
- "version_a01_apis",
25
- "const",
26
- "cloud_api",
27
27
  "roborock_typing",
28
28
  "exceptions",
29
- "data",
30
- # Add new APIs here in the future when they are public e.g. devices/
29
+ "const",
31
30
  ]
@@ -140,6 +140,9 @@ class RoborockBaseTimer(RoborockBase):
140
140
  else None
141
141
  )
142
142
 
143
+ def as_list(self) -> list:
144
+ return [self.start_hour, self.start_minute, self.end_hour, self.end_minute]
145
+
143
146
  def __repr__(self) -> str:
144
147
  return _attr_repr(self)
145
148
 
@@ -1,6 +1,8 @@
1
- # Roborock Device Discovery
1
+ # Roborock Devices & Discovery
2
2
 
3
- This page documents the full lifecycle of device discovery across Cloud and Network.
3
+ The devices module provides functionality to discover Roborock devices on the
4
+ network. This section documents the full lifecycle of device discovery across
5
+ Cloud and Network.
4
6
 
5
7
  ## Init account setup
6
8
 
@@ -61,7 +63,7 @@ that a newer version of the API should be used.
61
63
 
62
64
  ## Design
63
65
 
64
- ### Current API Issues
66
+ ### Prior API Issues
65
67
 
66
68
  - Complex Inheritance Hierarchy: Multiple inheritance with classes like RoborockMqttClientV1 inheriting from both RoborockMqttClient and RoborockClientV1
67
69
 
@@ -0,0 +1,11 @@
1
+ """
2
+ .. include:: ./README.md
3
+ """
4
+
5
+ __all__ = [
6
+ "device",
7
+ "device_manager",
8
+ "cache",
9
+ "file_cache",
10
+ "traits",
11
+ ]
@@ -4,12 +4,15 @@ This interface is experimental and subject to breaking changes without notice
4
4
  until the API is stable.
5
5
  """
6
6
 
7
+ import asyncio
8
+ import datetime
7
9
  import logging
8
10
  from abc import ABC
9
11
  from collections.abc import Callable, Mapping
10
12
  from typing import Any, TypeVar, cast
11
13
 
12
14
  from roborock.data import HomeDataDevice, HomeDataProduct
15
+ from roborock.exceptions import RoborockException
13
16
  from roborock.roborock_message import RoborockMessage
14
17
 
15
18
  from .channel import Channel
@@ -22,6 +25,11 @@ __all__ = [
22
25
  "RoborockDevice",
23
26
  ]
24
27
 
28
+ # Exponential backoff parameters
29
+ MIN_BACKOFF_INTERVAL = datetime.timedelta(seconds=10)
30
+ MAX_BACKOFF_INTERVAL = datetime.timedelta(minutes=30)
31
+ BACKOFF_MULTIPLIER = 1.5
32
+
25
33
 
26
34
  class RoborockDevice(ABC, TraitsMixin):
27
35
  """A generic channel for establishing a connection with a Roborock device.
@@ -54,6 +62,7 @@ class RoborockDevice(ABC, TraitsMixin):
54
62
  self._device_info = device_info
55
63
  self._product = product
56
64
  self._channel = channel
65
+ self._connect_task: asyncio.Task[None] | None = None
57
66
  self._unsub: Callable[[], None] | None = None
58
67
 
59
68
  @property
@@ -98,6 +107,38 @@ class RoborockDevice(ABC, TraitsMixin):
98
107
  """
99
108
  return self._channel.is_local_connected
100
109
 
110
+ def start_connect(self) -> None:
111
+ """Start a background task to connect to the device.
112
+
113
+ This will attempt to connect to the device using the appropriate protocol
114
+ channel. If the connection fails, it will retry with exponential backoff.
115
+
116
+ Once connected, the device will remain connected until `close()` is
117
+ called. The device will automatically attempt to reconnect if the connection
118
+ is lost.
119
+ """
120
+
121
+ async def connect_loop() -> None:
122
+ backoff = MIN_BACKOFF_INTERVAL
123
+ try:
124
+ while True:
125
+ try:
126
+ await self.connect()
127
+ return
128
+ except RoborockException as e:
129
+ _LOGGER.info("Failed to connect to device %s: %s", self.name, e)
130
+ _LOGGER.info(
131
+ "Retrying connection to device %s in %s seconds", self.name, backoff.total_seconds()
132
+ )
133
+ await asyncio.sleep(backoff.total_seconds())
134
+ backoff = min(backoff * BACKOFF_MULTIPLIER, MAX_BACKOFF_INTERVAL)
135
+ except asyncio.CancelledError:
136
+ _LOGGER.info("connect_loop for device %s was cancelled", self.name)
137
+ # Clean exit on cancellation
138
+ return
139
+
140
+ self._connect_task = asyncio.create_task(connect_loop())
141
+
101
142
  async def connect(self) -> None:
102
143
  """Connect to the device using the appropriate protocol channel."""
103
144
  if self._unsub:
@@ -107,6 +148,12 @@ class RoborockDevice(ABC, TraitsMixin):
107
148
 
108
149
  async def close(self) -> None:
109
150
  """Close all connections to the device."""
151
+ if self._connect_task:
152
+ self._connect_task.cancel()
153
+ try:
154
+ await self._connect_task
155
+ except asyncio.CancelledError:
156
+ pass
110
157
  if self._unsub:
111
158
  self._unsub()
112
159
  self._unsub = None
@@ -86,7 +86,7 @@ class DeviceManager:
86
86
  if duid in self._devices:
87
87
  continue
88
88
  new_device = self._device_creator(home_data, device, product)
89
- await new_device.connect()
89
+ new_device.start_connect()
90
90
  new_devices[duid] = new_device
91
91
 
92
92
  self._devices.update(new_devices)
@@ -0,0 +1,70 @@
1
+ """This module implements a file-backed cache for device information.
2
+
3
+ This module provides a `FileCache` class that implements the `Cache` protocol
4
+ to store and retrieve cached device information from a file on disk. This allows
5
+ persistent caching of device data across application restarts.
6
+ """
7
+
8
+ import asyncio
9
+ import pathlib
10
+ import pickle
11
+ from collections.abc import Callable
12
+ from typing import Any
13
+
14
+ from .cache import Cache, CacheData
15
+
16
+
17
+ class FileCache(Cache):
18
+ """File backed cache implementation."""
19
+
20
+ def __init__(self, file_path: pathlib.Path, init_fn: Callable[[], CacheData] = CacheData) -> None:
21
+ """Initialize the file cache with the given file path."""
22
+ self._init_fn = init_fn
23
+ self._file_path = file_path
24
+ self._cache_data: CacheData | None = None
25
+
26
+ async def get(self) -> CacheData:
27
+ """Get cached value."""
28
+ if self._cache_data is not None:
29
+ return self._cache_data
30
+
31
+ data = await load_value(self._file_path)
32
+ if data is not None and not isinstance(data, CacheData):
33
+ raise TypeError(f"Invalid cache data loaded from {self._file_path}")
34
+
35
+ self._cache_data = data or self._init_fn()
36
+ return self._cache_data
37
+
38
+ async def set(self, value: CacheData) -> None: # type: ignore[override]
39
+ """Set value in the cache."""
40
+ self._cache_data = value
41
+
42
+ async def flush(self) -> None:
43
+ """Flush the cache to disk."""
44
+ if self._cache_data is None:
45
+ return
46
+ await store_value(self._file_path, self._cache_data)
47
+
48
+
49
+ async def store_value(file_path: pathlib.Path, value: Any) -> None:
50
+ """Store a value to the given file path."""
51
+
52
+ def _store_to_disk(file_path: pathlib.Path, value: Any) -> None:
53
+ with open(file_path, "wb") as f:
54
+ data = pickle.dumps(value)
55
+ f.write(data)
56
+
57
+ await asyncio.to_thread(_store_to_disk, file_path, value)
58
+
59
+
60
+ async def load_value(file_path: pathlib.Path) -> Any | None:
61
+ """Load a value from the given file path."""
62
+
63
+ def _load_from_disk(file_path: pathlib.Path) -> Any | None:
64
+ if not file_path.exists():
65
+ return None
66
+ with open(file_path, "rb") as f:
67
+ data = f.read()
68
+ return pickle.loads(data)
69
+
70
+ return await asyncio.to_thread(_load_from_disk, file_path)
@@ -6,8 +6,7 @@ from roborock.devices.mqtt_channel import MqttChannel
6
6
  from roborock.devices.traits import Trait
7
7
  from roborock.roborock_message import RoborockB01Props
8
8
 
9
- __init__ = [
10
- "create_b01_traits",
9
+ __all__ = [
11
10
  "PropertiesApi",
12
11
  ]
13
12
 
@@ -67,27 +67,27 @@ from .wash_towel_mode import WashTowelModeTrait
67
67
  _LOGGER = logging.getLogger(__name__)
68
68
 
69
69
  __all__ = [
70
- "create",
71
70
  "PropertiesApi",
72
- "StatusTrait",
73
- "DoNotDisturbTrait",
74
- "CleanSummaryTrait",
75
- "SoundVolumeTrait",
76
- "MapsTrait",
77
- "MapContentTrait",
78
- "ConsumableTrait",
79
- "HomeTrait",
80
- "DeviceFeaturesTrait",
81
- "CommandTrait",
82
- "ChildLockTrait",
83
- "FlowLedStatusTrait",
84
- "LedStatusTrait",
85
- "ValleyElectricityTimerTrait",
86
- "DustCollectionModeTrait",
87
- "WashTowelModeTrait",
88
- "SmartWashParamsTrait",
89
- "NetworkInfoTrait",
90
- "RoutinesTrait",
71
+ "child_lock",
72
+ "clean_summary",
73
+ "common",
74
+ "consumeable",
75
+ "device_features",
76
+ "do_not_disturb",
77
+ "dust_collection_mode",
78
+ "flow_led_status",
79
+ "home",
80
+ "led_status",
81
+ "map_content",
82
+ "maps",
83
+ "network_info",
84
+ "rooms",
85
+ "routines",
86
+ "smart_wash_params",
87
+ "status",
88
+ "valley_electricity_timer",
89
+ "volume",
90
+ "wash_towel_mode",
91
91
  ]
92
92
 
93
93
 
@@ -14,7 +14,7 @@ class CleanSummaryTrait(CleanSummaryWithDetail, common.V1TraitMixin):
14
14
 
15
15
  command = RoborockCommand.GET_CLEAN_SUMMARY
16
16
 
17
- async def refresh(self) -> Self:
17
+ async def refresh(self) -> None:
18
18
  """Refresh the clean summary data and last clean record.
19
19
 
20
20
  Assumes that the clean summary has already been fetched.
@@ -23,10 +23,9 @@ class CleanSummaryTrait(CleanSummaryWithDetail, common.V1TraitMixin):
23
23
  if not self.records:
24
24
  _LOGGER.debug("No clean records available in clean summary.")
25
25
  self.last_clean_record = None
26
- return self
26
+ return
27
27
  last_record_id = self.records[-1]
28
28
  self.last_clean_record = await self.get_clean_record(last_record_id)
29
- return self
30
29
 
31
30
  @classmethod
32
31
  def _parse_type_response(cls, response: common.V1ResponseData) -> Self:
@@ -76,7 +76,7 @@ class V1TraitMixin(ABC):
76
76
  raise ValueError("Device trait in invalid state")
77
77
  return self._rpc_channel
78
78
 
79
- async def refresh(self) -> Self:
79
+ async def refresh(self) -> None:
80
80
  """Refresh the contents of this trait."""
81
81
  response = await self.rpc_channel.send_command(self.command)
82
82
  new_data = self._parse_response(response)
@@ -84,7 +84,6 @@ class V1TraitMixin(ABC):
84
84
  raise ValueError(f"Internal error, unexpected response type: {new_data!r}")
85
85
  _LOGGER.debug("Refreshed %s: %s", self.__class__.__name__, new_data)
86
86
  self._update_trait_values(new_data)
87
- return self
88
87
 
89
88
  def _update_trait_values(self, new_data: RoborockBase) -> None:
90
89
  """Update the values of this trait from another instance."""
@@ -1,5 +1,4 @@
1
1
  from dataclasses import fields
2
- from typing import Self
3
2
 
4
3
  from roborock.data import AppInitStatus, RoborockProductNickname
5
4
  from roborock.device_features import DeviceFeatures
@@ -13,7 +12,7 @@ class DeviceFeaturesTrait(DeviceFeatures, common.V1TraitMixin):
13
12
 
14
13
  command = RoborockCommand.APP_GET_INIT_STATUS
15
14
 
16
- def __init__(self, product_nickname: RoborockProductNickname, cache: Cache) -> None:
15
+ def __init__(self, product_nickname: RoborockProductNickname, cache: Cache) -> None: # pylint: disable=super-init-not-called
17
16
  """Initialize MapContentTrait."""
18
17
  self._nickname = product_nickname
19
18
  self._cache = cache
@@ -22,7 +21,7 @@ class DeviceFeaturesTrait(DeviceFeatures, common.V1TraitMixin):
22
21
  for field in fields(self):
23
22
  setattr(self, field.name, False)
24
23
 
25
- async def refresh(self) -> Self:
24
+ async def refresh(self) -> None:
26
25
  """Refresh the contents of this trait.
27
26
 
28
27
  This will use cached device features if available since they do not
@@ -32,12 +31,11 @@ class DeviceFeaturesTrait(DeviceFeatures, common.V1TraitMixin):
32
31
  cache_data = await self._cache.get()
33
32
  if cache_data.device_features is not None:
34
33
  self._update_trait_values(cache_data.device_features)
35
- return self
34
+ return
36
35
  # Save cached device features
37
- device_features = await super().refresh()
38
- cache_data.device_features = device_features
36
+ await super().refresh()
37
+ cache_data.device_features = self
39
38
  await self._cache.set(cache_data)
40
- return device_features
41
39
 
42
40
  def _parse_response(self, response: common.V1ResponseData) -> DeviceFeatures:
43
41
  """Parse the response from the device into a MapContentTrait instance."""
@@ -17,7 +17,7 @@ class DoNotDisturbTrait(DnDTimer, common.V1TraitMixin, common.RoborockSwitchBase
17
17
 
18
18
  async def set_dnd_timer(self, dnd_timer: DnDTimer) -> None:
19
19
  """Set the Do Not Disturb (DND) timer settings of the device."""
20
- await self.rpc_channel.send_command(RoborockCommand.SET_DND_TIMER, params=dnd_timer.as_dict())
20
+ await self.rpc_channel.send_command(RoborockCommand.SET_DND_TIMER, params=dnd_timer.as_list())
21
21
 
22
22
  async def clear_dnd_timer(self) -> None:
23
23
  """Clear the Do Not Disturb (DND) timer settings of the device."""
@@ -27,18 +27,9 @@ class DoNotDisturbTrait(DnDTimer, common.V1TraitMixin, common.RoborockSwitchBase
27
27
  """Set the Do Not Disturb (DND) timer settings of the device."""
28
28
  await self.rpc_channel.send_command(
29
29
  RoborockCommand.SET_DND_TIMER,
30
- params={
31
- **self.as_dict(),
32
- _ENABLED_PARAM: 1,
33
- },
30
+ params=self.as_list(),
34
31
  )
35
32
 
36
33
  async def disable(self) -> None:
37
- """Set the Do Not Disturb (DND) timer settings of the device."""
38
- await self.rpc_channel.send_command(
39
- RoborockCommand.SET_DND_TIMER,
40
- params={
41
- **self.as_dict(),
42
- _ENABLED_PARAM: 0,
43
- },
44
- )
34
+ """Disable the Do Not Disturb (DND) timer settings of the device."""
35
+ await self.rpc_channel.send_command(RoborockCommand.CLOSE_DND_TIMER)
@@ -158,7 +158,7 @@ class HomeTrait(RoborockBase, common.V1TraitMixin):
158
158
  home_map_info[map_info.map_flag] = combined_map_info
159
159
  return home_map_info, home_map_content
160
160
 
161
- async def refresh(self) -> Self:
161
+ async def refresh(self) -> None:
162
162
  """Refresh current map's underlying map and room data, updating cache as needed.
163
163
 
164
164
  This will only refresh the current map's data and will not populate non
@@ -171,7 +171,7 @@ class HomeTrait(RoborockBase, common.V1TraitMixin):
171
171
  # then we'll fall through below to refresh the current map only.
172
172
  try:
173
173
  await self.discover_home()
174
- return self
174
+ return
175
175
  except RoborockDeviceBusy:
176
176
  _LOGGER.debug("Cannot refresh home data while device is busy cleaning")
177
177
 
@@ -189,7 +189,6 @@ class HomeTrait(RoborockBase, common.V1TraitMixin):
189
189
  await self._update_current_map(
190
190
  map_flag, combined_map_info, new_map_content, update_cache=self._discovery_completed
191
191
  )
192
- return self
193
192
 
194
193
  @property
195
194
  def home_map_info(self) -> dict[int, CombinedMapInfo] | None:
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
- from typing import Self
7
6
 
8
7
  from roborock.data import NetworkInfo
9
8
  from roborock.devices.cache import Cache
@@ -25,20 +24,20 @@ class NetworkInfoTrait(NetworkInfo, common.V1TraitMixin):
25
24
 
26
25
  command = RoborockCommand.GET_NETWORK_INFO
27
26
 
28
- def __init__(self, device_uid: str, cache: Cache) -> None:
27
+ def __init__(self, device_uid: str, cache: Cache) -> None: # pylint: disable=super-init-not-called
29
28
  """Initialize the trait."""
30
29
  self._device_uid = device_uid
31
30
  self._cache = cache
32
31
  self.ip = ""
33
32
 
34
- async def refresh(self) -> Self:
33
+ async def refresh(self) -> None:
35
34
  """Refresh the network info from the cache."""
36
35
 
37
36
  cache_data = await self._cache.get()
38
37
  if cache_data.network_info and (network_info := cache_data.network_info.get(self._device_uid)):
39
38
  _LOGGER.debug("Using cached network info for device %s", self._device_uid)
40
39
  self._update_trait_values(network_info)
41
- return self
40
+ return
42
41
 
43
42
  # Load from device if not in cache
44
43
  _LOGGER.debug("No cached network info for device %s, fetching from device", self._device_uid)
@@ -48,8 +47,6 @@ class NetworkInfoTrait(NetworkInfo, common.V1TraitMixin):
48
47
  cache_data.network_info[self._device_uid] = self
49
48
  await self._cache.set(cache_data)
50
49
 
51
- return self
52
-
53
50
  def _parse_response(self, response: common.V1ResponseData) -> NetworkInfo:
54
51
  """Parse the response from the device into a NetworkInfo."""
55
52
  if not isinstance(response, dict):
@@ -18,7 +18,7 @@ class ValleyElectricityTimerTrait(ValleyElectricityTimer, common.V1TraitMixin, c
18
18
 
19
19
  async def set_timer(self, timer: ValleyElectricityTimer) -> None:
20
20
  """Set the Valley Electricity Timer settings of the device."""
21
- await self.rpc_channel.send_command(RoborockCommand.SET_VALLEY_ELECTRICITY_TIMER, params=timer.as_dict())
21
+ await self.rpc_channel.send_command(RoborockCommand.SET_VALLEY_ELECTRICITY_TIMER, params=timer.as_list())
22
22
 
23
23
  async def clear_timer(self) -> None:
24
24
  """Clear the Valley Electricity Timer settings of the device."""
@@ -28,18 +28,11 @@ class ValleyElectricityTimerTrait(ValleyElectricityTimer, common.V1TraitMixin, c
28
28
  """Enable the Valley Electricity Timer settings of the device."""
29
29
  await self.rpc_channel.send_command(
30
30
  RoborockCommand.SET_VALLEY_ELECTRICITY_TIMER,
31
- params={
32
- **self.as_dict(),
33
- _ENABLED_PARAM: 1,
34
- },
31
+ params=self.as_list(),
35
32
  )
36
33
 
37
34
  async def disable(self) -> None:
38
35
  """Disable the Valley Electricity Timer settings of the device."""
39
36
  await self.rpc_channel.send_command(
40
- RoborockCommand.SET_VALLEY_ELECTRICITY_TIMER,
41
- params={
42
- **self.as_dict(),
43
- _ENABLED_PARAM: 0,
44
- },
37
+ RoborockCommand.CLOSE_VALLEY_ELECTRICITY_TIMER,
45
38
  )
@@ -0,0 +1,3 @@
1
+ """Protocols for communicating with Roborock devices."""
2
+
3
+ __all__: list[str] = []
@@ -1,7 +0,0 @@
1
- """The devices module provides functionality to discover Roborock devices on the network."""
2
-
3
- __all__ = [
4
- "device",
5
- "device_manager",
6
- "cache",
7
- ]
File without changes