python-roborock 2.40.0__tar.gz → 2.41.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 (57) hide show
  1. {python_roborock-2.40.0 → python_roborock-2.41.0}/PKG-INFO +1 -1
  2. {python_roborock-2.40.0 → python_roborock-2.41.0}/pyproject.toml +1 -1
  3. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/containers.py +15 -14
  4. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/devices/device_manager.py +2 -0
  5. python_roborock-2.41.0/roborock/devices/traits/dnd.py +41 -0
  6. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/devices/traits/status.py +3 -3
  7. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/devices/v1_rpc_channel.py +6 -2
  8. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/protocols/v1_protocol.py +7 -6
  9. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/version_1_apis/roborock_client_v1.py +9 -8
  10. {python_roborock-2.40.0 → python_roborock-2.41.0}/LICENSE +0 -0
  11. {python_roborock-2.40.0 → python_roborock-2.41.0}/README.md +0 -0
  12. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/__init__.py +0 -0
  13. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/api.py +0 -0
  14. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/b01_containers.py +0 -0
  15. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/broadcast_protocol.py +0 -0
  16. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/callbacks.py +0 -0
  17. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/clean_modes.py +0 -0
  18. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/cli.py +0 -0
  19. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/cloud_api.py +0 -0
  20. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/code_mappings.py +0 -0
  21. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/command_cache.py +0 -0
  22. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/const.py +0 -0
  23. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/device_features.py +0 -0
  24. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/devices/README.md +0 -0
  25. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/devices/__init__.py +0 -0
  26. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/devices/a01_channel.py +0 -0
  27. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/devices/b01_channel.py +0 -0
  28. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/devices/cache.py +0 -0
  29. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/devices/channel.py +0 -0
  30. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/devices/device.py +0 -0
  31. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/devices/local_channel.py +0 -0
  32. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/devices/mqtt_channel.py +0 -0
  33. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/devices/traits/b01/__init__.py +0 -0
  34. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/devices/traits/b01/props.py +0 -0
  35. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/devices/traits/dyad.py +0 -0
  36. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/devices/traits/trait.py +0 -0
  37. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/devices/traits/zeo.py +0 -0
  38. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/devices/v1_channel.py +0 -0
  39. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/exceptions.py +0 -0
  40. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/mqtt/__init__.py +0 -0
  41. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/mqtt/roborock_session.py +0 -0
  42. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/mqtt/session.py +0 -0
  43. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/protocol.py +0 -0
  44. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/protocols/a01_protocol.py +0 -0
  45. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/protocols/b01_protocol.py +0 -0
  46. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/py.typed +0 -0
  47. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/roborock_future.py +0 -0
  48. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/roborock_message.py +0 -0
  49. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/roborock_typing.py +0 -0
  50. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/util.py +0 -0
  51. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/version_1_apis/__init__.py +0 -0
  52. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
  53. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  54. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/version_a01_apis/__init__.py +0 -0
  55. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  56. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
  57. {python_roborock-2.40.0 → python_roborock-2.41.0}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-roborock
3
- Version: 2.40.0
3
+ Version: 2.41.0
4
4
  Summary: A package to control Roborock vacuums.
5
5
  Home-page: https://github.com/humbertogontijo/python-roborock
6
6
  License: GPL-3.0-only
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-roborock"
3
- version = "2.40.0"
3
+ version = "2.41.0"
4
4
  description = "A package to control Roborock vacuums."
5
5
  authors = ["humbertogontijo <humbertogontijo@users.noreply.github.com>"]
6
6
  license = "GPL-3.0-only"
@@ -109,8 +109,6 @@ def _decamelize(s: str):
109
109
 
110
110
  @dataclass
111
111
  class RoborockBase:
112
- _ignore_keys = [] # type: ignore
113
-
114
112
  @staticmethod
115
113
  def _convert_to_class_obj(class_type: type, value):
116
114
  if get_origin(class_type) is list:
@@ -134,8 +132,8 @@ class RoborockBase:
134
132
  return None
135
133
  field_types = {field.name: field.type for field in dataclasses.fields(cls)}
136
134
  result: dict[str, Any] = {}
137
- for key, value in data.items():
138
- key = _decamelize(key)
135
+ for orig_key, value in data.items():
136
+ key = _decamelize(orig_key)
139
137
  if (field_type := field_types.get(key)) is None:
140
138
  continue
141
139
  if value == "None" or value is None:
@@ -178,16 +176,18 @@ class RoborockBaseTimer(RoborockBase):
178
176
  end_hour: int | None = None
179
177
  end_minute: int | None = None
180
178
  enabled: int | None = None
181
- start_time: datetime.time | None = None
182
- end_time: datetime.time | None = None
183
179
 
184
- def __post_init__(self) -> None:
185
- self.start_time = (
180
+ @property
181
+ def start_time(self) -> datetime.time | None:
182
+ return (
186
183
  datetime.time(hour=self.start_hour, minute=self.start_minute)
187
184
  if self.start_hour is not None and self.start_minute is not None
188
185
  else None
189
186
  )
190
- self.end_time = (
187
+
188
+ @property
189
+ def end_time(self) -> datetime.time | None:
190
+ return (
191
191
  datetime.time(hour=self.end_hour, minute=self.end_minute)
192
192
  if self.end_hour is not None and self.end_minute is not None
193
193
  else None
@@ -684,19 +684,20 @@ class MultiMapsListMapInfoBakMaps(RoborockBase):
684
684
 
685
685
  @dataclass
686
686
  class MultiMapsListMapInfo(RoborockBase):
687
- _ignore_keys = ["mapFlag"]
688
-
689
- mapFlag: int
687
+ map_flag: int
690
688
  name: str
691
689
  add_time: Any | None = None
692
690
  length: Any | None = None
693
691
  bak_maps: list[MultiMapsListMapInfoBakMaps] | None = None
694
692
 
693
+ @property
694
+ def mapFlag(self) -> int:
695
+ """Alias for map_flag, returns the map flag as an integer."""
696
+ return self.map_flag
697
+
695
698
 
696
699
  @dataclass
697
700
  class MultiMapsList(RoborockBase):
698
- _ignore_keys = ["mapFlag"]
699
-
700
701
  max_multi_map: int | None = None
701
702
  max_bak_map: int | None = None
702
703
  multi_map_count: int | None = None
@@ -22,6 +22,7 @@ from .cache import Cache, NoCache
22
22
  from .channel import Channel
23
23
  from .mqtt_channel import create_mqtt_channel
24
24
  from .traits.b01.props import B01PropsApi
25
+ from .traits.dnd import DoNotDisturbTrait
25
26
  from .traits.dyad import DyadApi
26
27
  from .traits.status import StatusTrait
27
28
  from .traits.trait import Trait
@@ -152,6 +153,7 @@ async def create_device_manager(
152
153
  case DeviceVersion.V1:
153
154
  channel = create_v1_channel(user_data, mqtt_params, mqtt_session, device, cache)
154
155
  traits.append(StatusTrait(product, channel.rpc_channel))
156
+ traits.append(DoNotDisturbTrait(channel.rpc_channel))
155
157
  case DeviceVersion.A01:
156
158
  mqtt_channel = create_mqtt_channel(user_data, mqtt_params, mqtt_session, device)
157
159
  match product.category:
@@ -0,0 +1,41 @@
1
+ """Module for Roborock V1 devices.
2
+
3
+ This interface is experimental and subject to breaking changes without notice
4
+ until the API is stable.
5
+ """
6
+
7
+ import logging
8
+
9
+ from roborock.containers import DnDTimer
10
+ from roborock.devices.v1_rpc_channel import V1RpcChannel
11
+ from roborock.roborock_typing import RoborockCommand
12
+
13
+ from .trait import Trait
14
+
15
+ _LOGGER = logging.getLogger(__name__)
16
+
17
+ __all__ = [
18
+ "DoNotDisturbTrait",
19
+ ]
20
+
21
+
22
+ class DoNotDisturbTrait(Trait):
23
+ """Trait for managing Do Not Disturb (DND) settings on Roborock devices."""
24
+
25
+ name = "do_not_disturb"
26
+
27
+ def __init__(self, rpc_channel: V1RpcChannel) -> None:
28
+ """Initialize the DoNotDisturbTrait."""
29
+ self._rpc_channel = rpc_channel
30
+
31
+ async def get_dnd_timer(self) -> DnDTimer:
32
+ """Get the current Do Not Disturb (DND) timer settings of the device."""
33
+ return await self._rpc_channel.send_command(RoborockCommand.GET_DND_TIMER, response_type=DnDTimer)
34
+
35
+ async def set_dnd_timer(self, dnd_timer: DnDTimer) -> None:
36
+ """Set the Do Not Disturb (DND) timer settings of the device."""
37
+ await self._rpc_channel.send_command(RoborockCommand.SET_DND_TIMER, params=dnd_timer.as_dict())
38
+
39
+ async def clear_dnd_timer(self) -> None:
40
+ """Clear the Do Not Disturb (DND) timer settings of the device."""
41
+ await self._rpc_channel.send_command(RoborockCommand.CLOSE_DND_TIMER)
@@ -12,20 +12,20 @@ from roborock.containers import (
12
12
  S7MaxVStatus,
13
13
  Status,
14
14
  )
15
+ from roborock.devices.v1_rpc_channel import V1RpcChannel
15
16
  from roborock.roborock_typing import RoborockCommand
16
17
 
17
- from ..v1_rpc_channel import V1RpcChannel
18
18
  from .trait import Trait
19
19
 
20
20
  _LOGGER = logging.getLogger(__name__)
21
21
 
22
22
  __all__ = [
23
- "Status",
23
+ "StatusTrait",
24
24
  ]
25
25
 
26
26
 
27
27
  class StatusTrait(Trait):
28
- """Unified Roborock device class with automatic connection setup."""
28
+ """Trait for managing the status of Roborock devices."""
29
29
 
30
30
  name = "status"
31
31
 
@@ -132,8 +132,10 @@ class PayloadEncodedV1RpcChannel(BaseV1RpcChannel):
132
132
  params: ParamsType = None,
133
133
  ) -> Any:
134
134
  """Send a command and return a parsed response RoborockBase type."""
135
- _LOGGER.debug("Sending command (%s): %s, params=%s", self._name, method, params)
136
135
  request_message = RequestMessage(method, params=params)
136
+ _LOGGER.debug(
137
+ "Sending command (%s, request_id=%s): %s, params=%s", self._name, request_message.request_id, method, params
138
+ )
137
139
  message = self._payload_encoder(request_message)
138
140
 
139
141
  future: asyncio.Future[dict[str, Any]] = asyncio.Future()
@@ -141,8 +143,10 @@ class PayloadEncodedV1RpcChannel(BaseV1RpcChannel):
141
143
  def find_response(response_message: RoborockMessage) -> None:
142
144
  try:
143
145
  decoded = decode_rpc_response(response_message)
144
- except RoborockException:
146
+ except RoborockException as ex:
147
+ _LOGGER.debug("Exception while decoding message (%s): %s", response_message, ex)
145
148
  return
149
+ _LOGGER.debug("Received response (request_id=%s): %s", self._name, decoded.request_id)
146
150
  if decoded.request_id == request_message.request_id:
147
151
  future.set_result(decoded.data)
148
152
 
@@ -109,7 +109,7 @@ class ResponseMessage:
109
109
  def decode_rpc_response(message: RoborockMessage) -> ResponseMessage:
110
110
  """Decode a V1 RPC_RESPONSE message."""
111
111
  if not message.payload:
112
- raise RoborockException("Invalid V1 message format: missing payload")
112
+ return ResponseMessage(request_id=message.seq, data={})
113
113
  try:
114
114
  payload = json.loads(message.payload.decode())
115
115
  except (json.JSONDecodeError, TypeError) as e:
@@ -141,6 +141,8 @@ def decode_rpc_response(message: RoborockMessage) -> ResponseMessage:
141
141
  _LOGGER.debug("Decoded V1 message result: %s", result)
142
142
  if isinstance(result, list) and result:
143
143
  result = result[0]
144
+ if isinstance(result, str) and result == "ok":
145
+ result = {}
144
146
  if not isinstance(result, dict):
145
147
  raise RoborockException(f"Invalid V1 message format: 'result' should be a dictionary for {message.payload!r}")
146
148
  return ResponseMessage(request_id=request_id, data=result)
@@ -157,19 +159,18 @@ class MapResponse:
157
159
  """The map data, decrypted and decompressed."""
158
160
 
159
161
 
160
- def create_map_response_decoder(security_data: SecurityData) -> Callable[[RoborockMessage], MapResponse]:
162
+ def create_map_response_decoder(security_data: SecurityData) -> Callable[[RoborockMessage], MapResponse | None]:
161
163
  """Create a decoder for V1 map response messages."""
162
164
 
163
- def _decode_map_response(message: RoborockMessage) -> MapResponse:
165
+ def _decode_map_response(message: RoborockMessage) -> MapResponse | None:
164
166
  """Decode a V1 map response message."""
165
167
  if not message.payload or len(message.payload) < 24:
166
168
  raise RoborockException("Invalid V1 map response format: missing payload")
167
169
  header, body = message.payload[:24], message.payload[24:]
168
170
  [endpoint, _, request_id, _] = struct.unpack("<8s8sH6s", header)
169
171
  if not endpoint.decode().startswith(security_data.endpoint):
170
- raise RoborockException(
171
- f"Invalid V1 map response endpoint: {endpoint!r}, expected {security_data.endpoint!r}"
172
- )
172
+ _LOGGER.debug("Received map response requested not made by this device, ignoring.")
173
+ return None
173
174
  try:
174
175
  decrypted = Utils.decrypt_cbc(body, security_data.nonce)
175
176
  except ValueError as err:
@@ -150,7 +150,7 @@ class RoborockClientV1(RoborockClient, ABC):
150
150
  """Roborock client base class for version 1 devices."""
151
151
 
152
152
  _listeners: dict[str, ListenerModel] = {}
153
- _map_response_decoder: Callable[[RoborockMessage], MapResponse] | None = None
153
+ _map_response_decoder: Callable[[RoborockMessage], MapResponse | None] | None = None
154
154
 
155
155
  def __init__(self, device_info: DeviceData, security_data: SecurityData | None) -> None:
156
156
  """Initializes the Roborock client."""
@@ -439,13 +439,14 @@ class RoborockClientV1(RoborockClient, ABC):
439
439
  elif data.payload and protocol == RoborockMessageProtocol.MAP_RESPONSE:
440
440
  if self._map_response_decoder is not None:
441
441
  map_response = self._map_response_decoder(data)
442
- queue = self._waiting_queue.get(map_response.request_id)
443
- if queue:
444
- queue.set_result(map_response.data)
445
- else:
446
- self._logger.debug(
447
- "Received unsolicited map response for request_id %s", map_response.request_id
448
- )
442
+ if map_response is not None:
443
+ queue = self._waiting_queue.get(map_response.request_id)
444
+ if queue:
445
+ queue.set_result(map_response.data)
446
+ else:
447
+ self._logger.debug(
448
+ "Received unsolicited map response for request_id %s", map_response.request_id
449
+ )
449
450
  else:
450
451
  queue = self._waiting_queue.get(data.seq)
451
452
  if queue: