Skip to content

Commit 8898119

Browse files
committed
feat: add l01 discovery
1 parent 62f19ca commit 8898119

File tree

3 files changed

+76
-7
lines changed

3 files changed

+76
-7
lines changed

roborock/broadcast_protocol.py

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
from __future__ import annotations
22

33
import asyncio
4+
import hashlib
45
import json
56
import logging
67
from asyncio import BaseTransport, Lock
78

89
from construct import ( # type: ignore
910
Bytes,
1011
Checksum,
12+
GreedyBytes,
1113
Int16ub,
1214
Int32ub,
15+
Prefixed,
1316
RawCopy,
1417
Struct,
1518
)
19+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
1620

1721
from roborock.containers import BroadcastMessage
1822
from roborock.protocol import EncryptionAdapter, Utils, _Parser
@@ -29,14 +33,37 @@ def __init__(self, timeout: int = 5):
2933
self.devices_found: list[BroadcastMessage] = []
3034
self._mutex = Lock()
3135

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)
36+
def datagram_received(self, data: bytes, _):
37+
"""Handle incoming broadcast datagrams."""
38+
try:
39+
version = data[:3]
40+
if version == b"L01":
41+
[parsed_msg], _ = L01Parser.parse(data)
42+
encrypted_payload = parsed_msg.payload
3843

39-
async def discover(self):
44+
key = hashlib.sha256(BROADCAST_TOKEN).digest()
45+
iv_digest_input = data[:9]
46+
digest = hashlib.sha256(iv_digest_input).digest()
47+
iv = digest[:12]
48+
49+
cipher = AESGCM(key)
50+
decrypted_payload_bytes = cipher.decrypt(iv, encrypted_payload, None)
51+
json_payload = json.loads(decrypted_payload_bytes)
52+
parsed_message = BroadcastMessage(duid=json_payload["duid"], ip=json_payload["ip"], version=version)
53+
_LOGGER.debug(f"Received L01 broadcast: {parsed_message}")
54+
self.devices_found.append(parsed_message)
55+
else:
56+
# Fallback to the original protocol parser for other versions
57+
[broadcast_message], _ = BroadcastParser.parse(data)
58+
if broadcast_message.payload:
59+
json_payload = json.loads(broadcast_message.payload)
60+
parsed_message = BroadcastMessage(duid=json_payload["duid"], ip=json_payload["ip"], version=version)
61+
_LOGGER.debug(f"Received broadcast: {parsed_message}")
62+
self.devices_found.append(parsed_message)
63+
except Exception as e:
64+
_LOGGER.warning(f"Failed to decode message: {bytes}. Error: {e}")
65+
66+
async def discover(self) -> list[BroadcastMessage]:
4067
async with self._mutex:
4168
try:
4269
loop = asyncio.get_event_loop()
@@ -64,5 +91,19 @@ def close(self):
6491
"checksum" / Checksum(Int32ub, Utils.crc, lambda ctx: ctx.message.data),
6592
)
6693

94+
_L01BroadcastMessage = Struct(
95+
"message"
96+
/ RawCopy(
97+
Struct(
98+
"version" / Bytes(3),
99+
"field1" / Bytes(4), # Unknown field
100+
"field2" / Bytes(2), # Unknown field
101+
"payload" / Prefixed(Int16ub, GreedyBytes), # Encrypted payload with length prefix
102+
)
103+
),
104+
"checksum" / Checksum(Int32ub, Utils.crc, lambda ctx: ctx.message.data),
105+
)
106+
67107

68108
BroadcastParser: _Parser = _Parser(_BroadcastMessage, False)
109+
L01Parser: _Parser = _Parser(_L01BroadcastMessage, False)

roborock/containers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,7 @@ class FlowLedStatus(RoborockBase):
783783
class BroadcastMessage(RoborockBase):
784784
duid: str
785785
ip: str
786+
version: bytes
786787

787788

788789
class ServerTimer(NamedTuple):

tests/test_broadcast_protocol.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from roborock.broadcast_protocol import RoborockProtocol
2+
3+
4+
def test_l01_data():
5+
data = bytes.fromhex(
6+
"4c30310000000000000043841496d5a31e34b5b02c1867c445509ba5a21aec1fa4b307bddeb27a75d9b366193e8a97d0534dc39851c"
7+
"980609f2670cdcaee04594ec5c93e3c5ae609b0c9a203139ac8e40c8c"
8+
)
9+
prot = RoborockProtocol()
10+
prot.datagram_received(data, None)
11+
device = prot.devices_found[0]
12+
assert device.duid == "ZrQn1jfZtJQLoPOL7620e"
13+
assert device.ip == "192.168.1.4"
14+
assert device.version == b"L01"
15+
16+
17+
def test_v1_data():
18+
data = bytes.fromhex(
19+
"312e30000003e003e80040b87035058b439f36af42f249605f8661897173f111bb849a6231831f5874a0cf220a25872ea412d796b4902ee"
20+
"57fdc120074b901b482acb1fe6d06317e3a72ddac654fe0"
21+
)
22+
prot = RoborockProtocol()
23+
prot.datagram_received(data, None)
24+
device = prot.devices_found[0]
25+
assert device.duid == "h96rOV3e8DTPMAOLiypREl"
26+
assert device.ip == "192.168.20.250"
27+
assert device.version == b"1.0"

0 commit comments

Comments
 (0)