pyezvizapi 1.0.1.7__py3-none-any.whl → 1.0.1.8__py3-none-any.whl
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.
Potentially problematic release.
This version of pyezvizapi might be problematic. Click here for more details.
- pyezvizapi/__init__.py +11 -1
- pyezvizapi/__main__.py +406 -283
- pyezvizapi/camera.py +488 -118
- pyezvizapi/cas.py +36 -43
- pyezvizapi/client.py +785 -1345
- pyezvizapi/constants.py +6 -1
- pyezvizapi/exceptions.py +9 -9
- pyezvizapi/light_bulb.py +80 -31
- pyezvizapi/models.py +103 -0
- pyezvizapi/mqtt.py +42 -14
- pyezvizapi/test_cam_rtsp.py +95 -109
- pyezvizapi/test_mqtt.py +101 -30
- pyezvizapi/utils.py +0 -1
- {pyezvizapi-1.0.1.7.dist-info → pyezvizapi-1.0.1.8.dist-info}/METADATA +2 -2
- pyezvizapi-1.0.1.8.dist-info/RECORD +21 -0
- pyezvizapi-1.0.1.7.dist-info/RECORD +0 -20
- {pyezvizapi-1.0.1.7.dist-info → pyezvizapi-1.0.1.8.dist-info}/WHEEL +0 -0
- {pyezvizapi-1.0.1.7.dist-info → pyezvizapi-1.0.1.8.dist-info}/entry_points.txt +0 -0
- {pyezvizapi-1.0.1.7.dist-info → pyezvizapi-1.0.1.8.dist-info}/licenses/LICENSE +0 -0
- {pyezvizapi-1.0.1.7.dist-info → pyezvizapi-1.0.1.8.dist-info}/licenses/LICENSE.md +0 -0
- {pyezvizapi-1.0.1.7.dist-info → pyezvizapi-1.0.1.8.dist-info}/top_level.txt +0 -0
pyezvizapi/cas.py
CHANGED
|
@@ -1,46 +1,50 @@
|
|
|
1
1
|
"""pyezvizapi CAS API Functions."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
from io import BytesIO
|
|
4
6
|
from itertools import cycle
|
|
7
|
+
import logging
|
|
5
8
|
import random
|
|
6
9
|
import socket
|
|
7
10
|
import ssl
|
|
11
|
+
from typing import Any, cast
|
|
8
12
|
|
|
9
13
|
from Crypto.Cipher import AES
|
|
10
14
|
import xmltodict
|
|
11
15
|
|
|
12
16
|
from .constants import FEATURE_CODE, XOR_KEY
|
|
13
|
-
from .exceptions import InvalidHost
|
|
17
|
+
from .exceptions import InvalidHost, PyEzvizError
|
|
18
|
+
|
|
19
|
+
_LOGGER = logging.getLogger(__name__)
|
|
14
20
|
|
|
15
21
|
|
|
16
|
-
def xor_enc_dec(msg, xor_key=XOR_KEY):
|
|
17
|
-
"""
|
|
22
|
+
def xor_enc_dec(msg: bytes, xor_key: bytes = XOR_KEY) -> bytes:
|
|
23
|
+
"""XOR encode/decode bytes with the given key."""
|
|
18
24
|
with BytesIO(msg) as stream:
|
|
19
|
-
|
|
20
|
-
return xor_msg
|
|
25
|
+
return bytes(a ^ b for a, b in zip(stream.read(), cycle(xor_key)))
|
|
21
26
|
|
|
22
27
|
|
|
23
28
|
class EzvizCAS:
|
|
24
29
|
"""Ezviz CAS server client."""
|
|
25
30
|
|
|
26
|
-
def __init__(self, token) -> None:
|
|
31
|
+
def __init__(self, token: dict[str, Any] | None) -> None:
|
|
27
32
|
"""Initialize the client object."""
|
|
28
33
|
self._session = None
|
|
29
|
-
self._token = token or {
|
|
34
|
+
self._token: dict[str, Any] = token or {
|
|
30
35
|
"session_id": None,
|
|
31
36
|
"rf_session_id": None,
|
|
32
37
|
"username": None,
|
|
33
38
|
"api_url": "apiieu.ezvizlife.com",
|
|
34
39
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"""Fetch encryption code from ezviz cas server."""
|
|
40
|
+
if not token or "service_urls" not in token:
|
|
41
|
+
raise PyEzvizError("Missing service_urls in token; call EzvizClient.login() first")
|
|
42
|
+
self._service_urls: dict[str, Any] = token["service_urls"]
|
|
39
43
|
|
|
44
|
+
def cas_get_encryption(self, devserial: str) -> dict[str, Any]:
|
|
45
|
+
"""Fetch encryption code from EZVIZ CAS server."""
|
|
40
46
|
# Random hex 64 characters long.
|
|
41
|
-
|
|
42
|
-
rand_hex = "%064x" % rand_hex
|
|
43
|
-
rand_hex = rand_hex[:64]
|
|
47
|
+
rand_hex_str = f"{random.randrange(10**80):064x}"[:64]
|
|
44
48
|
|
|
45
49
|
payload = (
|
|
46
50
|
f"\x9e\xba\xac\xe9\x01\x00\x00\x00\x00\x00"
|
|
@@ -55,18 +59,17 @@ class EzvizCAS:
|
|
|
55
59
|
f"\n\t<ClientType>0</ClientType>\n</Request>\n"
|
|
56
60
|
).encode("latin1")
|
|
57
61
|
|
|
58
|
-
payload_end_padding =
|
|
62
|
+
payload_end_padding = rand_hex_str.encode("latin1")
|
|
59
63
|
|
|
60
64
|
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
|
|
61
|
-
|
|
62
65
|
context.set_ciphers(
|
|
63
66
|
"DEFAULT:!aNULL:!eNULL:!MD5:!3DES:!DES:!RC4:!IDEA:!SEED:!aDSS:!SRP:!PSK"
|
|
64
67
|
)
|
|
65
68
|
|
|
66
69
|
# Create a TCP/IP socket
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
)
|
|
70
|
+
host = cast(str, self._service_urls["sysConf"][15])
|
|
71
|
+
port = cast(int, self._service_urls["sysConf"][16])
|
|
72
|
+
my_socket = socket.create_connection((host, port))
|
|
70
73
|
my_socket = context.wrap_socket(
|
|
71
74
|
my_socket, server_hostname=self._service_urls["sysConf"][15]
|
|
72
75
|
)
|
|
@@ -74,29 +77,22 @@ class EzvizCAS:
|
|
|
74
77
|
# Get CAS Encryption Key
|
|
75
78
|
try:
|
|
76
79
|
my_socket.send(payload + payload_end_padding)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
response_bytes = my_socket.recv(1024)
|
|
81
|
+
_LOGGER.debug("Get Encryption Key: %r", response_bytes)
|
|
80
82
|
except (socket.gaierror, ConnectionRefusedError) as err:
|
|
81
83
|
raise InvalidHost("Invalid IP or Hostname") from err
|
|
82
|
-
|
|
83
84
|
finally:
|
|
84
85
|
my_socket.close()
|
|
85
86
|
|
|
86
87
|
# Trim header, tail and convert xml to dict.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
body = response_bytes[32:-32]
|
|
89
|
+
doc = xmltodict.parse(body)
|
|
90
|
+
return cast(dict[str, Any], doc)
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def set_camera_defence_state(self, serial, enable=1):
|
|
92
|
+
def set_camera_defence_state(self, serial: str, enable: int = 1) -> bool:
|
|
94
93
|
"""Enable alarm notifications."""
|
|
95
|
-
|
|
96
94
|
# Random hex 64 characters long.
|
|
97
|
-
|
|
98
|
-
rand_hex = "%064x" % rand_hex
|
|
99
|
-
rand_hex = rand_hex[:64]
|
|
95
|
+
rand_hex_str = f"{random.randrange(10**80):064x}"[:64]
|
|
100
96
|
|
|
101
97
|
payload = (
|
|
102
98
|
f"\x9e\xba\xac\xe9\x01\x00\x00\x00\x00\x00"
|
|
@@ -114,7 +110,7 @@ class EzvizCAS:
|
|
|
114
110
|
f"\x00\x00\x00\xb0\x00\x00\x00\x00"
|
|
115
111
|
).encode("latin1")
|
|
116
112
|
|
|
117
|
-
payload_end_padding =
|
|
113
|
+
payload_end_padding = rand_hex_str.encode("latin1")
|
|
118
114
|
|
|
119
115
|
# xor camera serial
|
|
120
116
|
xor_cam_serial = xor_enc_dec(serial.encode("latin1"))
|
|
@@ -130,15 +126,14 @@ class EzvizCAS:
|
|
|
130
126
|
).encode("latin1")
|
|
131
127
|
|
|
132
128
|
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
|
|
133
|
-
|
|
134
129
|
context.set_ciphers(
|
|
135
130
|
"DEFAULT:!aNULL:!eNULL:!MD5:!3DES:!DES:!RC4:!IDEA:!SEED:!aDSS:!SRP:!PSK"
|
|
136
131
|
)
|
|
137
132
|
|
|
138
133
|
# Create a TCP/IP socket
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
)
|
|
134
|
+
host = cast(str, self._service_urls["sysConf"][15])
|
|
135
|
+
port = cast(int, self._service_urls["sysConf"][16])
|
|
136
|
+
my_socket = socket.create_connection((host, port))
|
|
142
137
|
my_socket = context.wrap_socket(
|
|
143
138
|
my_socket, server_hostname=self._service_urls["sysConf"][15]
|
|
144
139
|
)
|
|
@@ -156,13 +151,11 @@ class EzvizCAS:
|
|
|
156
151
|
cipher = AES.new(aes_key, AES.MODE_CBC, iv_value)
|
|
157
152
|
|
|
158
153
|
try:
|
|
159
|
-
|
|
160
|
-
my_socket.send(payload +
|
|
161
|
-
|
|
162
|
-
|
|
154
|
+
enc_bytes = cipher.encrypt(defence_msg_string)
|
|
155
|
+
my_socket.send(payload + enc_bytes + payload_end_padding)
|
|
156
|
+
_LOGGER.debug("Set camera response: %r", my_socket.recv())
|
|
163
157
|
except (socket.gaierror, ConnectionRefusedError) as err:
|
|
164
158
|
raise InvalidHost("Invalid IP or Hostname") from err
|
|
165
|
-
|
|
166
159
|
finally:
|
|
167
160
|
my_socket.close()
|
|
168
161
|
|