Skip to content
This repository was archived by the owner on Mar 25, 2024. It is now read-only.

Commit 1ccf6bd

Browse files
committed
Implement P2P protocol for device control
1 parent 6ba258a commit 1ccf6bd

File tree

13 files changed

+996
-69
lines changed

13 files changed

+996
-69
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ coverage.xml
66
poetry.lock
77
tags
88
__pycache__
9+
.vscode/

AUTHORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
* Aaron Bach (https://github.com/bachya)
44
* Alexander Bessonov (https://github.com/nonsleepr)
55
* Fuzzy (https://github.com/FuzzyMistborn)
6+
* Keshav (https://github.com/keshavdv)

eufy_security/api.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
from .camera import Camera
1010
from .errors import InvalidCredentialsError, RequestError, raise_error
11+
from .station import Station
12+
from .types import DeviceType
1113

1214
_LOGGER: logging.Logger = logging.getLogger(__name__)
1315

@@ -28,6 +30,7 @@ def __init__(self, email: str, password: str, websession: ClientSession) -> None
2830
self._token_expiration: Optional[datetime] = None
2931

3032
self.cameras: Dict[str, Camera] = {}
33+
self.stations: Dict[str, Station] = {}
3134

3235
async def async_authenticate(self) -> None:
3336
"""Authenticate and get an access token."""
@@ -54,17 +57,33 @@ async def async_get_history(self) -> dict:
5457

5558
async def async_update_device_info(self) -> None:
5659
"""Get the latest device info."""
60+
# Cameras
5761
devices_resp = await self.request("post", "app/get_devs_list")
5862

5963
if not devices_resp.get("data"):
6064
return
6165

6266
for device_info in devices_resp["data"]:
63-
if device_info["device_sn"] in self.cameras:
64-
camera = self.cameras[device_info["device_sn"]]
65-
camera.camera_info = device_info
66-
continue
67-
self.cameras[device_info["device_sn"]] = Camera(self, device_info)
67+
if DeviceType(device_info["device_type"]).is_camera():
68+
if device_info["device_sn"] in self.cameras:
69+
camera = self.cameras[device_info["device_sn"]]
70+
camera.camera_info = device_info
71+
continue
72+
self.cameras[device_info["device_sn"]] = Camera(self, device_info)
73+
74+
# Stations
75+
stations_resp = await self.request("post", "app/get_hub_list")
76+
77+
if not stations_resp.get("data"):
78+
return
79+
80+
for device_info in stations_resp["data"]:
81+
if DeviceType(device_info["device_type"]).is_station():
82+
if device_info["station_sn"] in self.stations:
83+
station = self.stations[device_info["station_sn"]]
84+
station.station_info = device_info
85+
continue
86+
self.stations[device_info["station_sn"]] = Station(self, device_info)
6887

6988
async def request(
7089
self,

eufy_security/camera.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import logging
33
from typing import TYPE_CHECKING
44

5-
from .params import ParamType
5+
from .types import DeviceType, ParamType
66

77
if TYPE_CHECKING:
88
from .api import API # pylint: disable=cyclic-import
@@ -18,6 +18,11 @@ def __init__(self, api: "API", camera_info: dict) -> None:
1818
self._api = api
1919
self.camera_info: dict = camera_info
2020

21+
@property
22+
def device_type(self) -> str:
23+
"""Return the station's device type."""
24+
return DeviceType(self.camera_info["device_type"])
25+
2126
@property
2227
def hardware_version(self) -> str:
2328
"""Return the camera's hardware version."""

eufy_security/errors.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ class RequestError(EufySecurityError):
2020
pass
2121

2222

23+
class EufySecurityP2PError(Exception):
24+
"""Define a P2P related error."""
25+
26+
pass
27+
28+
2329
ERRORS: Dict[int, Type[EufySecurityError]] = {26006: InvalidCredentialsError}
2430

2531

eufy_security/p2p/discovery.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import asyncio
2+
3+
from .lib32100 import BaseP2PClientProtocol
4+
from .types import (
5+
P2PClientProtocolRequestMessageType,
6+
P2PClientProtocolResponseMessageType,
7+
)
8+
9+
10+
class DiscoveryP2PClientProtocol(BaseP2PClientProtocol):
11+
def __init__(self, loop, p2p_did: str, key: str, on_conn_lost):
12+
self.loop = loop
13+
self.p2p_did = p2p_did
14+
self.key = key
15+
self.on_conn_lost = on_conn_lost
16+
self.transport = None
17+
self.addresses = []
18+
self.response_count = 0
19+
20+
def connection_made(self, transport):
21+
self.transport = transport
22+
23+
# Build payload
24+
p2p_did_components = self.p2p_did.split("-")
25+
payload = bytearray(p2p_did_components[0].encode())
26+
payload.extend(int(p2p_did_components[1]).to_bytes(5, byteorder="big"))
27+
payload.extend(p2p_did_components[2].encode())
28+
payload.extend([0x00, 0x00, 0x00, 0x00, 0x00])
29+
ip, port = self.transport.get_extra_info("sockname")
30+
payload.extend(port.to_bytes(2, byteorder="little"))
31+
payload.extend([int(x) for x in ip.split(".")[::-1]])
32+
payload.extend(
33+
[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00]
34+
)
35+
payload.extend(self.key.encode())
36+
payload.extend([0x00, 0x00, 0x00, 0x00])
37+
38+
self.transport.sendto(
39+
self.create_message(
40+
P2PClientProtocolRequestMessageType.LOOKUP_WITH_KEY, payload
41+
)
42+
)
43+
# Manually timeout if we don't get an answer
44+
self.loop.create_task(self.timeout(1.5))
45+
46+
async def timeout(self, seconds: float):
47+
await asyncio.sleep(seconds)
48+
self.transport.close()
49+
50+
def process_response(
51+
self, msg_type: P2PClientProtocolResponseMessageType, payload: bytes
52+
):
53+
msg = payload[2:]
54+
if msg_type == P2PClientProtocolResponseMessageType.LOOKUP_ADDR:
55+
port = payload[5] * 256 + payload[4]
56+
ip = f"{payload[9]}.{payload[8]}.{payload[7]}.{payload[6]}"
57+
self.addresses.append((ip, port))
58+
59+
# We expect at most two IP/port combos so we can bail early
60+
# if we received both
61+
self.response_count += 1
62+
if self.response_count == 2:
63+
self.transport.close()
64+
65+
def error_received(self, exc):
66+
_LOGGER.exception("Error received", exc_info=exc)
67+
68+
def connection_lost(self, exc):
69+
self.on_conn_lost.set_result(self.addresses)

eufy_security/p2p/lib32100.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import math
2+
3+
from .types import (
4+
P2PClientProtocolRequestMessageType,
5+
P2PClientProtocolResponseMessageType,
6+
)
7+
8+
9+
class BaseP2PClientProtocol:
10+
def create_message(
11+
self, msg_type: P2PClientProtocolRequestMessageType, payload=bytearray()
12+
):
13+
msg = bytearray()
14+
msg.extend(msg_type.value)
15+
payload_size = len(payload)
16+
msg.append(math.floor(payload_size / 256))
17+
msg.append(payload_size % 256)
18+
msg.extend(payload)
19+
return msg
20+
21+
def datagram_received(self, data, _):
22+
msg_type = P2PClientProtocolResponseMessageType(bytes(data[0:2]))
23+
payload = data[2:]
24+
self.process_response(msg_type, payload)
25+
26+
def process_response(
27+
self, msg_type: P2PClientProtocolResponseMessageType, payload: bytes
28+
):
29+
raise NotImplementedError()

0 commit comments

Comments
 (0)