python-roborock 2.39.2__tar.gz → 2.40.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_roborock-2.39.2 → python_roborock-2.40.1}/PKG-INFO +1 -1
- {python_roborock-2.39.2 → python_roborock-2.40.1}/pyproject.toml +1 -1
- python_roborock-2.40.1/roborock/broadcast_protocol.py +114 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/containers.py +1 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/protocol.py +2 -2
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/protocols/v1_protocol.py +4 -5
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/version_1_apis/roborock_client_v1.py +9 -8
- python_roborock-2.39.2/roborock/broadcast_protocol.py +0 -68
- {python_roborock-2.39.2 → python_roborock-2.40.1}/LICENSE +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/README.md +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/__init__.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/api.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/b01_containers.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/callbacks.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/clean_modes.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/cli.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/cloud_api.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/code_mappings.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/command_cache.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/const.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/device_features.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/devices/README.md +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/devices/__init__.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/devices/a01_channel.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/devices/b01_channel.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/devices/cache.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/devices/channel.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/devices/device.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/devices/device_manager.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/devices/local_channel.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/devices/mqtt_channel.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/devices/traits/b01/__init__.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/devices/traits/b01/props.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/devices/traits/dyad.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/devices/traits/status.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/devices/traits/trait.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/devices/traits/zeo.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/devices/v1_channel.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/devices/v1_rpc_channel.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/exceptions.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/mqtt/roborock_session.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/mqtt/session.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/protocols/a01_protocol.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/protocols/b01_protocol.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/py.typed +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/roborock_future.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/roborock_message.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/roborock_typing.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/util.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/version_1_apis/__init__.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/version_a01_apis/__init__.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
- {python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/web_api.py +0 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from asyncio import BaseTransport, Lock
|
|
8
|
+
|
|
9
|
+
from construct import ( # type: ignore
|
|
10
|
+
Bytes,
|
|
11
|
+
Checksum,
|
|
12
|
+
GreedyBytes,
|
|
13
|
+
Int16ub,
|
|
14
|
+
Int32ub,
|
|
15
|
+
Prefixed,
|
|
16
|
+
RawCopy,
|
|
17
|
+
Struct,
|
|
18
|
+
)
|
|
19
|
+
from Crypto.Cipher import AES
|
|
20
|
+
|
|
21
|
+
from roborock import RoborockException
|
|
22
|
+
from roborock.containers import BroadcastMessage
|
|
23
|
+
from roborock.protocol import EncryptionAdapter, Utils, _Parser
|
|
24
|
+
|
|
25
|
+
_LOGGER = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
BROADCAST_TOKEN = b"qWKYcdQWrbm9hPqe"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RoborockProtocol(asyncio.DatagramProtocol):
|
|
31
|
+
def __init__(self, timeout: int = 5):
|
|
32
|
+
self.timeout = timeout
|
|
33
|
+
self.transport: BaseTransport | None = None
|
|
34
|
+
self.devices_found: list[BroadcastMessage] = []
|
|
35
|
+
self._mutex = Lock()
|
|
36
|
+
|
|
37
|
+
def datagram_received(self, data: bytes, _):
|
|
38
|
+
"""Handle incoming broadcast datagrams."""
|
|
39
|
+
try:
|
|
40
|
+
version = data[:3]
|
|
41
|
+
if version == b"L01":
|
|
42
|
+
[parsed_msg], _ = L01Parser.parse(data)
|
|
43
|
+
encrypted_payload = parsed_msg.payload
|
|
44
|
+
if encrypted_payload is None:
|
|
45
|
+
raise RoborockException("No encrypted payload found in broadcast message")
|
|
46
|
+
ciphertext = encrypted_payload[:-16]
|
|
47
|
+
tag = encrypted_payload[-16:]
|
|
48
|
+
|
|
49
|
+
key = hashlib.sha256(BROADCAST_TOKEN).digest()
|
|
50
|
+
iv_digest_input = data[:9]
|
|
51
|
+
digest = hashlib.sha256(iv_digest_input).digest()
|
|
52
|
+
iv = digest[:12]
|
|
53
|
+
|
|
54
|
+
cipher = AES.new(key, AES.MODE_GCM, nonce=iv)
|
|
55
|
+
decrypted_payload_bytes = cipher.decrypt_and_verify(ciphertext, tag)
|
|
56
|
+
json_payload = json.loads(decrypted_payload_bytes)
|
|
57
|
+
parsed_message = BroadcastMessage(duid=json_payload["duid"], ip=json_payload["ip"], version=version)
|
|
58
|
+
_LOGGER.debug(f"Received L01 broadcast: {parsed_message}")
|
|
59
|
+
self.devices_found.append(parsed_message)
|
|
60
|
+
else:
|
|
61
|
+
# Fallback to the original protocol parser for other versions
|
|
62
|
+
[broadcast_message], _ = BroadcastParser.parse(data)
|
|
63
|
+
if broadcast_message.payload:
|
|
64
|
+
json_payload = json.loads(broadcast_message.payload)
|
|
65
|
+
parsed_message = BroadcastMessage(duid=json_payload["duid"], ip=json_payload["ip"], version=version)
|
|
66
|
+
_LOGGER.debug(f"Received broadcast: {parsed_message}")
|
|
67
|
+
self.devices_found.append(parsed_message)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
_LOGGER.warning(f"Failed to decode message: {data!r}. Error: {e}")
|
|
70
|
+
|
|
71
|
+
async def discover(self) -> list[BroadcastMessage]:
|
|
72
|
+
async with self._mutex:
|
|
73
|
+
try:
|
|
74
|
+
loop = asyncio.get_event_loop()
|
|
75
|
+
self.transport, _ = await loop.create_datagram_endpoint(lambda: self, local_addr=("0.0.0.0", 58866))
|
|
76
|
+
await asyncio.sleep(self.timeout)
|
|
77
|
+
return self.devices_found
|
|
78
|
+
finally:
|
|
79
|
+
self.close()
|
|
80
|
+
self.devices_found = []
|
|
81
|
+
|
|
82
|
+
def close(self):
|
|
83
|
+
self.transport.close() if self.transport else None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
_BroadcastMessage = Struct(
|
|
87
|
+
"message"
|
|
88
|
+
/ RawCopy(
|
|
89
|
+
Struct(
|
|
90
|
+
"version" / Bytes(3),
|
|
91
|
+
"seq" / Int32ub,
|
|
92
|
+
"protocol" / Int16ub,
|
|
93
|
+
"payload" / EncryptionAdapter(lambda ctx: BROADCAST_TOKEN),
|
|
94
|
+
)
|
|
95
|
+
),
|
|
96
|
+
"checksum" / Checksum(Int32ub, Utils.crc, lambda ctx: ctx.message.data),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
_L01BroadcastMessage = Struct(
|
|
100
|
+
"message"
|
|
101
|
+
/ RawCopy(
|
|
102
|
+
Struct(
|
|
103
|
+
"version" / Bytes(3),
|
|
104
|
+
"field1" / Bytes(4), # Unknown field
|
|
105
|
+
"field2" / Bytes(2), # Unknown field
|
|
106
|
+
"payload" / Prefixed(Int16ub, GreedyBytes), # Encrypted payload with length prefix
|
|
107
|
+
)
|
|
108
|
+
),
|
|
109
|
+
"checksum" / Checksum(Int32ub, Utils.crc, lambda ctx: ctx.message.data),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
BroadcastParser: _Parser = _Parser(_BroadcastMessage, False)
|
|
114
|
+
L01Parser: _Parser = _Parser(_L01BroadcastMessage, False)
|
|
@@ -304,10 +304,10 @@ class _Parser:
|
|
|
304
304
|
messages.append(
|
|
305
305
|
RoborockMessage(
|
|
306
306
|
version=message.message.value.version,
|
|
307
|
-
seq=message.message.value.seq,
|
|
307
|
+
seq=message.message.value.get("seq"),
|
|
308
308
|
random=message.message.value.get("random"),
|
|
309
309
|
timestamp=message.message.value.get("timestamp"),
|
|
310
|
-
protocol=message.message.value.protocol,
|
|
310
|
+
protocol=message.message.value.get("protocol"),
|
|
311
311
|
payload=message.message.value.payload,
|
|
312
312
|
)
|
|
313
313
|
)
|
|
@@ -157,19 +157,18 @@ class MapResponse:
|
|
|
157
157
|
"""The map data, decrypted and decompressed."""
|
|
158
158
|
|
|
159
159
|
|
|
160
|
-
def create_map_response_decoder(security_data: SecurityData) -> Callable[[RoborockMessage], MapResponse]:
|
|
160
|
+
def create_map_response_decoder(security_data: SecurityData) -> Callable[[RoborockMessage], MapResponse | None]:
|
|
161
161
|
"""Create a decoder for V1 map response messages."""
|
|
162
162
|
|
|
163
|
-
def _decode_map_response(message: RoborockMessage) -> MapResponse:
|
|
163
|
+
def _decode_map_response(message: RoborockMessage) -> MapResponse | None:
|
|
164
164
|
"""Decode a V1 map response message."""
|
|
165
165
|
if not message.payload or len(message.payload) < 24:
|
|
166
166
|
raise RoborockException("Invalid V1 map response format: missing payload")
|
|
167
167
|
header, body = message.payload[:24], message.payload[24:]
|
|
168
168
|
[endpoint, _, request_id, _] = struct.unpack("<8s8sH6s", header)
|
|
169
169
|
if not endpoint.decode().startswith(security_data.endpoint):
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
)
|
|
170
|
+
_LOGGER.debug("Received map response requested not made by this device, ignoring.")
|
|
171
|
+
return None
|
|
173
172
|
try:
|
|
174
173
|
decrypted = Utils.decrypt_cbc(body, security_data.nonce)
|
|
175
174
|
except ValueError as err:
|
{python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/version_1_apis/roborock_client_v1.py
RENAMED
|
@@ -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
|
-
|
|
443
|
-
|
|
444
|
-
queue
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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:
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import json
|
|
5
|
-
import logging
|
|
6
|
-
from asyncio import BaseTransport, Lock
|
|
7
|
-
|
|
8
|
-
from construct import ( # type: ignore
|
|
9
|
-
Bytes,
|
|
10
|
-
Checksum,
|
|
11
|
-
Int16ub,
|
|
12
|
-
Int32ub,
|
|
13
|
-
RawCopy,
|
|
14
|
-
Struct,
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
from roborock.containers import BroadcastMessage
|
|
18
|
-
from roborock.protocol import EncryptionAdapter, Utils, _Parser
|
|
19
|
-
|
|
20
|
-
_LOGGER = logging.getLogger(__name__)
|
|
21
|
-
|
|
22
|
-
BROADCAST_TOKEN = b"qWKYcdQWrbm9hPqe"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class RoborockProtocol(asyncio.DatagramProtocol):
|
|
26
|
-
def __init__(self, timeout: int = 5):
|
|
27
|
-
self.timeout = timeout
|
|
28
|
-
self.transport: BaseTransport | None = None
|
|
29
|
-
self.devices_found: list[BroadcastMessage] = []
|
|
30
|
-
self._mutex = Lock()
|
|
31
|
-
|
|
32
|
-
def datagram_received(self, data, _):
|
|
33
|
-
[broadcast_message], _ = BroadcastParser.parse(data)
|
|
34
|
-
if broadcast_message.payload:
|
|
35
|
-
parsed_message = BroadcastMessage.from_dict(json.loads(broadcast_message.payload))
|
|
36
|
-
_LOGGER.debug(f"Received broadcast: {parsed_message}")
|
|
37
|
-
self.devices_found.append(parsed_message)
|
|
38
|
-
|
|
39
|
-
async def discover(self):
|
|
40
|
-
async with self._mutex:
|
|
41
|
-
try:
|
|
42
|
-
loop = asyncio.get_event_loop()
|
|
43
|
-
self.transport, _ = await loop.create_datagram_endpoint(lambda: self, local_addr=("0.0.0.0", 58866))
|
|
44
|
-
await asyncio.sleep(self.timeout)
|
|
45
|
-
return self.devices_found
|
|
46
|
-
finally:
|
|
47
|
-
self.close()
|
|
48
|
-
self.devices_found = []
|
|
49
|
-
|
|
50
|
-
def close(self):
|
|
51
|
-
self.transport.close() if self.transport else None
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
_BroadcastMessage = Struct(
|
|
55
|
-
"message"
|
|
56
|
-
/ RawCopy(
|
|
57
|
-
Struct(
|
|
58
|
-
"version" / Bytes(3),
|
|
59
|
-
"seq" / Int32ub,
|
|
60
|
-
"protocol" / Int16ub,
|
|
61
|
-
"payload" / EncryptionAdapter(lambda ctx: BROADCAST_TOKEN),
|
|
62
|
-
)
|
|
63
|
-
),
|
|
64
|
-
"checksum" / Checksum(Int32ub, Utils.crc, lambda ctx: ctx.message.data),
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
BroadcastParser: _Parser = _Parser(_BroadcastMessage, False)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/version_1_apis/roborock_mqtt_client_v1.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-2.39.2 → python_roborock-2.40.1}/roborock/version_a01_apis/roborock_client_a01.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|