pyezvizapi 1.0.3.6__py3-none-any.whl → 1.0.3.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/__main__.py +2 -2
- pyezvizapi/client.py +10 -0
- pyezvizapi/constants.py +12 -2
- pyezvizapi/feature.py +67 -61
- pyezvizapi/light_bulb.py +1 -1
- pyezvizapi/mqtt.py +2 -2
- pyezvizapi/test_cam_rtsp.py +8 -10
- pyezvizapi/test_mqtt.py +11 -11
- pyezvizapi/utils.py +68 -48
- pyezvizapi-1.0.3.8.dist-info/METADATA +287 -0
- pyezvizapi-1.0.3.8.dist-info/RECORD +21 -0
- pyezvizapi-1.0.3.6.dist-info/METADATA +0 -27
- pyezvizapi-1.0.3.6.dist-info/RECORD +0 -22
- pyezvizapi-1.0.3.6.dist-info/entry_points.txt +0 -2
- {pyezvizapi-1.0.3.6.dist-info → pyezvizapi-1.0.3.8.dist-info}/WHEEL +0 -0
- {pyezvizapi-1.0.3.6.dist-info → pyezvizapi-1.0.3.8.dist-info}/licenses/LICENSE +0 -0
- {pyezvizapi-1.0.3.6.dist-info → pyezvizapi-1.0.3.8.dist-info}/licenses/LICENSE.md +0 -0
- {pyezvizapi-1.0.3.6.dist-info → pyezvizapi-1.0.3.8.dist-info}/top_level.txt +0 -0
pyezvizapi/__main__.py
CHANGED
|
@@ -421,7 +421,7 @@ def _handle_devices_light(args: argparse.Namespace, client: EzvizClient) -> int:
|
|
|
421
421
|
|
|
422
422
|
def _handle_pagelist(client: EzvizClient) -> int:
|
|
423
423
|
"""Output full pagelist (raw JSON) for exploration in editors like Notepad++."""
|
|
424
|
-
data = client.
|
|
424
|
+
data = client.get_page_list()
|
|
425
425
|
_write_json(data)
|
|
426
426
|
return 0
|
|
427
427
|
|
|
@@ -611,7 +611,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
611
611
|
return 2
|
|
612
612
|
finally:
|
|
613
613
|
if args.save_token and args.token_file:
|
|
614
|
-
_save_token_file(args.token_file,
|
|
614
|
+
_save_token_file(args.token_file, client.export_token())
|
|
615
615
|
client.close_session()
|
|
616
616
|
|
|
617
617
|
|
pyezvizapi/client.py
CHANGED
|
@@ -4398,6 +4398,16 @@ class EzvizClient:
|
|
|
4398
4398
|
json_key=None,
|
|
4399
4399
|
)
|
|
4400
4400
|
|
|
4401
|
+
def get_page_list(self) -> Any:
|
|
4402
|
+
"""Return the full pagelist payload without filtering."""
|
|
4403
|
+
|
|
4404
|
+
return self._get_page_list()
|
|
4405
|
+
|
|
4406
|
+
def export_token(self) -> dict[str, Any]:
|
|
4407
|
+
"""Return a shallow copy of the current authentication token."""
|
|
4408
|
+
|
|
4409
|
+
return dict(self._token)
|
|
4410
|
+
|
|
4401
4411
|
def get_device(self) -> Any:
|
|
4402
4412
|
"""Get ezviz devices filter."""
|
|
4403
4413
|
return self._api_get_pagelist(page_filter="CLOUD", json_key="deviceInfos")
|
pyezvizapi/constants.py
CHANGED
|
@@ -6,13 +6,23 @@ the Ezviz API to descriptive names.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from enum import Enum, unique
|
|
9
|
+
from hashlib import md5
|
|
10
|
+
import uuid
|
|
9
11
|
|
|
10
|
-
from .utils import generate_unique_code
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
def _generate_unique_code() -> str:
|
|
14
|
+
"""Generate a deterministic unique code for this host."""
|
|
15
|
+
|
|
16
|
+
mac_int = uuid.getnode()
|
|
17
|
+
mac_str = ":".join(f"{(mac_int >> i) & 0xFF:02x}" for i in range(40, -1, -8))
|
|
18
|
+
return md5(mac_str.encode("utf-8")).hexdigest()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
FEATURE_CODE = _generate_unique_code()
|
|
13
22
|
XOR_KEY = b"\x0c\x0eJ^X\x15@Rr"
|
|
14
23
|
DEFAULT_TIMEOUT = 25
|
|
15
24
|
MAX_RETRIES = 3
|
|
25
|
+
HIK_ENCRYPTION_HEADER = b"hikencodepicture"
|
|
16
26
|
REQUEST_HEADER = {
|
|
17
27
|
"featureCode": FEATURE_CODE,
|
|
18
28
|
"clientType": "3",
|
pyezvizapi/feature.py
CHANGED
|
@@ -5,22 +5,18 @@ from __future__ import annotations
|
|
|
5
5
|
from collections.abc import Iterable, Iterator, Mapping, MutableMapping
|
|
6
6
|
from typing import Any, cast
|
|
7
7
|
|
|
8
|
-
from .utils import coerce_int, decode_json
|
|
8
|
+
from .utils import WILDCARD_STEP, coerce_int, decode_json, first_nested
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def _feature_video_section(camera_data: Mapping[str, Any]) -> dict[str, Any]:
|
|
12
12
|
"""Return the nested Video feature section from feature info payload."""
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
video = group.get("Video")
|
|
21
|
-
if isinstance(video, MutableMapping):
|
|
22
|
-
return cast(dict[str, Any], video)
|
|
23
|
-
|
|
14
|
+
video = first_nested(
|
|
15
|
+
camera_data,
|
|
16
|
+
("FEATURE_INFO", WILDCARD_STEP, "Video"),
|
|
17
|
+
)
|
|
18
|
+
if isinstance(video, MutableMapping):
|
|
19
|
+
return cast(dict[str, Any], video)
|
|
24
20
|
return {}
|
|
25
21
|
|
|
26
22
|
|
|
@@ -192,8 +188,8 @@ def _normalize_port_list(value: Any) -> list[dict[str, Any]] | None:
|
|
|
192
188
|
return None
|
|
193
189
|
|
|
194
190
|
normalized: list[dict[str, Any]] = []
|
|
195
|
-
for
|
|
196
|
-
entry = decode_json(
|
|
191
|
+
for raw_entry in value:
|
|
192
|
+
entry = decode_json(raw_entry)
|
|
197
193
|
if not isinstance(entry, Mapping):
|
|
198
194
|
return None
|
|
199
195
|
port = coerce_int(entry.get("portNo"))
|
|
@@ -209,57 +205,68 @@ def normalize_port_security(payload: Any) -> dict[str, Any]:
|
|
|
209
205
|
|
|
210
206
|
seen: set[int] = set()
|
|
211
207
|
|
|
208
|
+
def _apply_hint(
|
|
209
|
+
candidate: dict[str, Any] | None, hint_value: bool | None
|
|
210
|
+
) -> dict[str, Any] | None:
|
|
211
|
+
if (
|
|
212
|
+
candidate is not None
|
|
213
|
+
and "enabled" not in candidate
|
|
214
|
+
and isinstance(hint_value, bool)
|
|
215
|
+
):
|
|
216
|
+
candidate["enabled"] = hint_value
|
|
217
|
+
return candidate
|
|
218
|
+
|
|
219
|
+
def _walk_mapping(obj: Mapping[str, Any], hint: bool | None) -> dict[str, Any] | None:
|
|
220
|
+
obj_id = id(obj)
|
|
221
|
+
if obj_id in seen:
|
|
222
|
+
return None
|
|
223
|
+
seen.add(obj_id)
|
|
224
|
+
|
|
225
|
+
enabled_local = obj.get("enabled")
|
|
226
|
+
if isinstance(enabled_local, bool):
|
|
227
|
+
hint = enabled_local
|
|
228
|
+
|
|
229
|
+
ports = _normalize_port_list(obj.get("portSecurityList"))
|
|
230
|
+
if ports is not None:
|
|
231
|
+
return {
|
|
232
|
+
"portSecurityList": ports,
|
|
233
|
+
"enabled": bool(enabled_local)
|
|
234
|
+
if isinstance(enabled_local, bool)
|
|
235
|
+
else bool(hint)
|
|
236
|
+
if isinstance(hint, bool)
|
|
237
|
+
else True,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
for key in ("PortSecurity", "value", "data", "NetworkSecurityProtection"):
|
|
241
|
+
if key in obj:
|
|
242
|
+
candidate = _apply_hint(_walk(obj[key], hint), hint)
|
|
243
|
+
if candidate:
|
|
244
|
+
return candidate
|
|
245
|
+
|
|
246
|
+
for value in obj.values():
|
|
247
|
+
candidate = _apply_hint(_walk(value, hint), hint)
|
|
248
|
+
if candidate:
|
|
249
|
+
return candidate
|
|
250
|
+
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
def _walk_iterable(values: Iterable[Any], hint: bool | None) -> dict[str, Any] | None:
|
|
254
|
+
for item in values:
|
|
255
|
+
candidate = _walk(item, hint)
|
|
256
|
+
if candidate:
|
|
257
|
+
return candidate
|
|
258
|
+
return None
|
|
259
|
+
|
|
212
260
|
def _walk(obj: Any, hint: bool | None = None) -> dict[str, Any] | None:
|
|
213
261
|
obj = decode_json(obj)
|
|
214
262
|
if obj is None:
|
|
215
263
|
return None
|
|
216
264
|
|
|
217
265
|
if isinstance(obj, Mapping):
|
|
218
|
-
|
|
219
|
-
if obj_id in seen:
|
|
220
|
-
return None
|
|
221
|
-
seen.add(obj_id)
|
|
222
|
-
|
|
223
|
-
enabled_local = obj.get("enabled")
|
|
224
|
-
if isinstance(enabled_local, bool):
|
|
225
|
-
hint = enabled_local
|
|
226
|
-
|
|
227
|
-
ports = _normalize_port_list(obj.get("portSecurityList"))
|
|
228
|
-
if ports is not None:
|
|
229
|
-
return {
|
|
230
|
-
"portSecurityList": ports,
|
|
231
|
-
"enabled": bool(enabled_local)
|
|
232
|
-
if isinstance(enabled_local, bool)
|
|
233
|
-
else bool(hint)
|
|
234
|
-
if isinstance(hint, bool)
|
|
235
|
-
else True,
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
for key in (
|
|
239
|
-
"PortSecurity",
|
|
240
|
-
"value",
|
|
241
|
-
"data",
|
|
242
|
-
"NetworkSecurityProtection",
|
|
243
|
-
):
|
|
244
|
-
if key in obj:
|
|
245
|
-
candidate = _walk(obj[key], hint)
|
|
246
|
-
if candidate:
|
|
247
|
-
if "enabled" not in candidate and isinstance(hint, bool):
|
|
248
|
-
candidate["enabled"] = hint
|
|
249
|
-
return candidate
|
|
250
|
-
|
|
251
|
-
for value in obj.values():
|
|
252
|
-
candidate = _walk(value, hint)
|
|
253
|
-
if candidate:
|
|
254
|
-
if "enabled" not in candidate and isinstance(hint, bool):
|
|
255
|
-
candidate["enabled"] = hint
|
|
256
|
-
return candidate
|
|
266
|
+
return _walk_mapping(obj, hint)
|
|
257
267
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
candidate = _walk(item, hint)
|
|
261
|
-
if candidate:
|
|
262
|
-
return candidate
|
|
268
|
+
if isinstance(obj, Iterable) and not isinstance(obj, (str, bytes, bytearray)):
|
|
269
|
+
return _walk_iterable(obj, hint)
|
|
263
270
|
|
|
264
271
|
return None
|
|
265
272
|
|
|
@@ -318,10 +325,9 @@ def display_mode_value(camera_data: Mapping[str, Any]) -> int:
|
|
|
318
325
|
display_mode = optionals.get("display_mode")
|
|
319
326
|
display_mode = decode_json(display_mode)
|
|
320
327
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
mode = display_mode
|
|
328
|
+
mode = (
|
|
329
|
+
display_mode.get("mode") if isinstance(display_mode, Mapping) else display_mode
|
|
330
|
+
)
|
|
325
331
|
|
|
326
332
|
if isinstance(mode, int) and mode in (1, 2, 3):
|
|
327
333
|
return mode
|
pyezvizapi/light_bulb.py
CHANGED
|
@@ -176,7 +176,7 @@ class EzvizLightBulb:
|
|
|
176
176
|
def set_brightness(self, value: int) -> bool:
|
|
177
177
|
"""Set the light bulb brightness.
|
|
178
178
|
|
|
179
|
-
The value must be in range 1
|
|
179
|
+
The value must be in range 1-100. Returns True on success.
|
|
180
180
|
|
|
181
181
|
Raises:
|
|
182
182
|
PyEzvizError: On API failures.
|
pyezvizapi/mqtt.py
CHANGED
|
@@ -83,7 +83,7 @@ class MqttData(TypedDict):
|
|
|
83
83
|
# Payload decoding helpers
|
|
84
84
|
# ---------------------------------------------------------------------------
|
|
85
85
|
|
|
86
|
-
# Field names in the comma
|
|
86
|
+
# Field names in the comma-separated ``ext`` payload from EZVIZ.
|
|
87
87
|
EXT_FIELD_NAMES: Final[tuple[str, ...]] = (
|
|
88
88
|
"channel_type",
|
|
89
89
|
"time",
|
|
@@ -247,7 +247,7 @@ class MQTTClient:
|
|
|
247
247
|
# Stop background thread and disconnect
|
|
248
248
|
self.mqtt_client.loop_stop()
|
|
249
249
|
self.mqtt_client.disconnect()
|
|
250
|
-
except
|
|
250
|
+
except (OSError, ValueError, RuntimeError) as err:
|
|
251
251
|
_LOGGER.debug("MQTT disconnect failed: %s", err)
|
|
252
252
|
# Always attempt to stop push on server side
|
|
253
253
|
self._stop_ezviz_push()
|
pyezvizapi/test_cam_rtsp.py
CHANGED
|
@@ -4,11 +4,14 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import base64
|
|
6
6
|
import hashlib
|
|
7
|
+
import logging
|
|
7
8
|
import socket
|
|
8
9
|
from typing import TypedDict
|
|
9
10
|
|
|
10
11
|
from .exceptions import AuthTestResultFailed, InvalidHost
|
|
11
12
|
|
|
13
|
+
_LOGGER = logging.getLogger(__name__)
|
|
14
|
+
|
|
12
15
|
|
|
13
16
|
def genmsg_describe(url: str, seq: int, user_agent: str, auth_seq: str) -> str:
|
|
14
17
|
"""Generate RTSP DESCRIBE request message."""
|
|
@@ -110,15 +113,14 @@ class TestRTSPAuth:
|
|
|
110
113
|
describe = genmsg_describe(
|
|
111
114
|
url, seq, self._rtsp_details["defaultUserAgent"], auth_seq
|
|
112
115
|
)
|
|
113
|
-
|
|
116
|
+
_LOGGER.debug("RTSP DESCRIBE (basic):\n%s", describe)
|
|
114
117
|
session.send(describe.encode())
|
|
115
118
|
msg1: bytes = session.recv(self._rtsp_details["bufLen"])
|
|
116
119
|
seq += 1
|
|
117
120
|
|
|
118
121
|
decoded = msg1.decode()
|
|
119
122
|
if "200 OK" in decoded:
|
|
120
|
-
|
|
121
|
-
print("Basic Auth test passed. Credentials Valid!")
|
|
123
|
+
_LOGGER.info("Basic auth result: %s", decoded)
|
|
122
124
|
return
|
|
123
125
|
|
|
124
126
|
if "Unauthorized" in decoded:
|
|
@@ -140,20 +142,16 @@ class TestRTSPAuth:
|
|
|
140
142
|
describe = genmsg_describe(
|
|
141
143
|
url, seq, self._rtsp_details["defaultUserAgent"], auth_seq
|
|
142
144
|
)
|
|
143
|
-
|
|
145
|
+
_LOGGER.debug("RTSP DESCRIBE (digest):\n%s", describe)
|
|
144
146
|
session.send(describe.encode())
|
|
145
147
|
msg1 = session.recv(self._rtsp_details["bufLen"])
|
|
146
148
|
decoded = msg1.decode()
|
|
147
|
-
|
|
149
|
+
_LOGGER.info("Digest auth result: %s", decoded)
|
|
148
150
|
|
|
149
151
|
if "200 OK" in decoded:
|
|
150
|
-
print("Digest Auth test Passed. Credentials Valid!")
|
|
151
152
|
return
|
|
152
153
|
|
|
153
154
|
if "401 Unauthorized" in decoded:
|
|
154
155
|
raise AuthTestResultFailed("Credentials not valid!!")
|
|
155
156
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
# ruff: noqa: T201
|
|
157
|
+
_LOGGER.info("Basic Auth test passed. Credentials Valid!")
|
pyezvizapi/test_mqtt.py
CHANGED
|
@@ -20,13 +20,14 @@ from .client import EzvizClient
|
|
|
20
20
|
from .exceptions import EzvizAuthVerificationCode, PyEzvizError
|
|
21
21
|
|
|
22
22
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
|
23
|
+
_LOGGER = logging.getLogger(__name__)
|
|
23
24
|
|
|
24
25
|
LOG_FILE = Path("mqtt_messages.jsonl") # JSON Lines format
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
def message_handler(msg: dict[str, Any]) -> None:
|
|
28
29
|
"""Handle new MQTT messages by printing and saving them to a file."""
|
|
29
|
-
|
|
30
|
+
_LOGGER.info("📩 New MQTT message: %s", msg)
|
|
30
31
|
with LOG_FILE.open("a", encoding="utf-8") as f:
|
|
31
32
|
f.write(json.dumps(msg, ensure_ascii=False) + "\n")
|
|
32
33
|
|
|
@@ -40,7 +41,7 @@ def _load_token_file(path: str | None) -> dict[str, Any] | None:
|
|
|
40
41
|
try:
|
|
41
42
|
return cast(dict[str, Any], json.loads(p.read_text(encoding="utf-8")))
|
|
42
43
|
except (OSError, json.JSONDecodeError):
|
|
43
|
-
|
|
44
|
+
_LOGGER.warning("Failed to read token file: %s", p)
|
|
44
45
|
return None
|
|
45
46
|
|
|
46
47
|
|
|
@@ -50,9 +51,9 @@ def _save_token_file(path: str | None, token: dict[str, Any]) -> None:
|
|
|
50
51
|
p = Path(path)
|
|
51
52
|
try:
|
|
52
53
|
p.write_text(json.dumps(token, indent=2), encoding="utf-8")
|
|
53
|
-
|
|
54
|
+
_LOGGER.info("Saved token to %s", p)
|
|
54
55
|
except OSError:
|
|
55
|
-
|
|
56
|
+
_LOGGER.warning("Failed to save token file: %s", p)
|
|
56
57
|
|
|
57
58
|
|
|
58
59
|
def main(argv: list[str] | None = None) -> int:
|
|
@@ -87,7 +88,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
87
88
|
|
|
88
89
|
# If no token and missing username/password, prompt interactively
|
|
89
90
|
if not token and (not username or not password):
|
|
90
|
-
|
|
91
|
+
_LOGGER.info("No token found. Please enter Ezviz credentials")
|
|
91
92
|
if not username:
|
|
92
93
|
username = input("Username: ")
|
|
93
94
|
if not password:
|
|
@@ -107,7 +108,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
107
108
|
code_int = None
|
|
108
109
|
client.login(sms_code=code_int)
|
|
109
110
|
except PyEzvizError as exp:
|
|
110
|
-
|
|
111
|
+
_LOGGER.error("Login failed: %s", exp)
|
|
111
112
|
return 1
|
|
112
113
|
|
|
113
114
|
# Start MQTT client
|
|
@@ -115,21 +116,20 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
115
116
|
mqtt_client.connect()
|
|
116
117
|
|
|
117
118
|
try:
|
|
118
|
-
|
|
119
|
+
_LOGGER.info("Listening for MQTT messages... (Ctrl+C to quit)")
|
|
119
120
|
while True:
|
|
120
121
|
time.sleep(1)
|
|
121
122
|
except KeyboardInterrupt:
|
|
122
|
-
|
|
123
|
+
_LOGGER.info("Stopping listener (keyboard interrupt)")
|
|
123
124
|
finally:
|
|
124
125
|
mqtt_client.stop()
|
|
125
|
-
|
|
126
|
+
_LOGGER.info("Listener stopped")
|
|
126
127
|
|
|
127
128
|
if args.save_token and args.token_file:
|
|
128
|
-
_save_token_file(args.token_file,
|
|
129
|
+
_save_token_file(args.token_file, client.export_token())
|
|
129
130
|
|
|
130
131
|
return 0
|
|
131
132
|
|
|
132
133
|
|
|
133
134
|
if __name__ == "__main__":
|
|
134
135
|
sys.exit(main())
|
|
135
|
-
# ruff: noqa: T201
|
pyezvizapi/utils.py
CHANGED
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from collections.abc import Iterable, Iterator
|
|
5
6
|
import datetime
|
|
6
7
|
from hashlib import md5
|
|
7
8
|
import json
|
|
8
9
|
import logging
|
|
9
10
|
import re as _re
|
|
10
11
|
from typing import Any
|
|
11
|
-
import uuid
|
|
12
12
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
13
13
|
|
|
14
14
|
from Crypto.Cipher import AES
|
|
15
15
|
|
|
16
|
+
from .constants import HIK_ENCRYPTION_HEADER
|
|
16
17
|
from .exceptions import PyEzvizError
|
|
17
18
|
|
|
18
19
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -62,18 +63,64 @@ def convert_to_dict(data: Any) -> Any:
|
|
|
62
63
|
|
|
63
64
|
def string_to_list(data: Any, separator: str = ",") -> Any:
|
|
64
65
|
"""Convert a string representation of a list to a list."""
|
|
65
|
-
if isinstance(data, str):
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return data.split(separator)
|
|
66
|
+
if isinstance(data, str) and separator in data:
|
|
67
|
+
try:
|
|
68
|
+
# Attempt to convert the string into a list
|
|
69
|
+
return data.split(separator)
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
except AttributeError:
|
|
72
|
+
return data
|
|
73
73
|
|
|
74
74
|
return data
|
|
75
75
|
|
|
76
76
|
|
|
77
|
+
PathComponent = str | int
|
|
78
|
+
WILDCARD_STEP = "*"
|
|
79
|
+
_MISSING = object()
|
|
80
|
+
MILLISECONDS_THRESHOLD = 1e11
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def iter_nested(data: Any, path: Iterable[PathComponent]) -> Iterator[Any]:
|
|
84
|
+
"""Yield values reachable by following a dotted path with optional wildcards."""
|
|
85
|
+
|
|
86
|
+
current: list[Any] = [data]
|
|
87
|
+
|
|
88
|
+
for step in path:
|
|
89
|
+
next_level: list[Any] = []
|
|
90
|
+
for candidate in current:
|
|
91
|
+
if step == WILDCARD_STEP:
|
|
92
|
+
if isinstance(candidate, dict):
|
|
93
|
+
next_level.extend(candidate.values())
|
|
94
|
+
elif isinstance(candidate, (list, tuple)):
|
|
95
|
+
next_level.extend(candidate)
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
if isinstance(candidate, dict) and step in candidate:
|
|
99
|
+
next_level.append(candidate[step])
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
isinstance(candidate, (list, tuple))
|
|
104
|
+
and isinstance(step, int)
|
|
105
|
+
and -len(candidate) <= step < len(candidate)
|
|
106
|
+
):
|
|
107
|
+
next_level.append(candidate[step])
|
|
108
|
+
|
|
109
|
+
current = next_level
|
|
110
|
+
if not current:
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
yield from current
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def first_nested(
|
|
117
|
+
data: Any, path: Iterable[PathComponent], default: Any = None
|
|
118
|
+
) -> Any:
|
|
119
|
+
"""Return the first value produced by iter_nested or ``default``."""
|
|
120
|
+
|
|
121
|
+
return next(iter_nested(data, path), default)
|
|
122
|
+
|
|
123
|
+
|
|
77
124
|
def fetch_nested_value(data: Any, keys: list, default_value: Any = None) -> Any:
|
|
78
125
|
"""Fetch the value corresponding to the given nested keys in a dictionary.
|
|
79
126
|
|
|
@@ -88,14 +135,8 @@ def fetch_nested_value(data: Any, keys: list, default_value: Any = None) -> Any:
|
|
|
88
135
|
The value corresponding to the nested keys or the default value.
|
|
89
136
|
|
|
90
137
|
"""
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
data = data[key]
|
|
94
|
-
|
|
95
|
-
except (KeyError, TypeError):
|
|
96
|
-
return default_value
|
|
97
|
-
|
|
98
|
-
return data
|
|
138
|
+
value = first_nested(data, keys, _MISSING)
|
|
139
|
+
return default_value if value is _MISSING else value
|
|
99
140
|
|
|
100
141
|
|
|
101
142
|
def decrypt_image(input_data: bytes, password: str) -> bytes:
|
|
@@ -116,8 +157,10 @@ def decrypt_image(input_data: bytes, password: str) -> bytes:
|
|
|
116
157
|
raise PyEzvizError("Invalid image data")
|
|
117
158
|
|
|
118
159
|
# check header
|
|
119
|
-
|
|
120
|
-
|
|
160
|
+
header_len = len(HIK_ENCRYPTION_HEADER)
|
|
161
|
+
|
|
162
|
+
if input_data[:header_len] != HIK_ENCRYPTION_HEADER:
|
|
163
|
+
_LOGGER.debug("Image header doesn't contain %s", HIK_ENCRYPTION_HEADER)
|
|
121
164
|
return input_data
|
|
122
165
|
|
|
123
166
|
file_hash = input_data[16:48]
|
|
@@ -132,7 +175,7 @@ def decrypt_image(input_data: bytes, password: str) -> bytes:
|
|
|
132
175
|
next_chunk = b""
|
|
133
176
|
output_data = b""
|
|
134
177
|
finished = False
|
|
135
|
-
i = 48 # offset
|
|
178
|
+
i = 48 # offset HIK header + hash
|
|
136
179
|
chunk_size = 1024 * AES.block_size
|
|
137
180
|
while not finished:
|
|
138
181
|
chunk, next_chunk = next_chunk, cipher.decrypt(input_data[i : i + chunk_size])
|
|
@@ -195,30 +238,6 @@ def deep_merge(dict1: Any, dict2: Any) -> Any:
|
|
|
195
238
|
return merged
|
|
196
239
|
|
|
197
240
|
|
|
198
|
-
def generate_unique_code() -> str:
|
|
199
|
-
"""Generate a deterministic, platform-agnostic unique code for the current host.
|
|
200
|
-
|
|
201
|
-
This function retrieves the host's MAC address using Python's standard
|
|
202
|
-
`uuid.getnode()` (works on Windows, Linux, macOS), converts it to a
|
|
203
|
-
canonical string representation, and then hashes it using MD5 to produce
|
|
204
|
-
a fixed-length hexadecimal string.
|
|
205
|
-
|
|
206
|
-
Returns:
|
|
207
|
-
str: A 32-character hexadecimal string uniquely representing
|
|
208
|
-
the host's MAC address. For example:
|
|
209
|
-
'a94e6756hghjgfghg49e0f310d9e44a'.
|
|
210
|
-
|
|
211
|
-
Notes:
|
|
212
|
-
- The output is deterministic: the same machine returns the same code.
|
|
213
|
-
- If the MAC address changes (e.g., different network adapter),
|
|
214
|
-
the output will change.
|
|
215
|
-
- MD5 is used here only for ID generation, not for security.
|
|
216
|
-
"""
|
|
217
|
-
mac_int = uuid.getnode()
|
|
218
|
-
mac_str = ":".join(f"{(mac_int >> i) & 0xFF:02x}" for i in range(40, -1, -8))
|
|
219
|
-
return md5(mac_str.encode("utf-8")).hexdigest()
|
|
220
|
-
|
|
221
|
-
|
|
222
241
|
# ---------------------------------------------------------------------------
|
|
223
242
|
# Time helpers for alarm/motion handling
|
|
224
243
|
# ---------------------------------------------------------------------------
|
|
@@ -255,7 +274,7 @@ def normalize_alarm_time(
|
|
|
255
274
|
if epoch is not None:
|
|
256
275
|
try:
|
|
257
276
|
ts = float(epoch if not isinstance(epoch, str) else float(epoch))
|
|
258
|
-
if ts >
|
|
277
|
+
if ts > MILLISECONDS_THRESHOLD: # milliseconds
|
|
259
278
|
ts /= 1000.0
|
|
260
279
|
event_utc = datetime.datetime.fromtimestamp(ts, tz=datetime.UTC)
|
|
261
280
|
alarm_dt_local = event_utc.astimezone(tzinfo)
|
|
@@ -322,10 +341,11 @@ def compute_motion_from_alarm(
|
|
|
322
341
|
now_local = datetime.datetime.now(tz=tzinfo).replace(microsecond=0)
|
|
323
342
|
now_utc = datetime.datetime.now(tz=datetime.UTC).replace(microsecond=0)
|
|
324
343
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
344
|
+
delta = (
|
|
345
|
+
now_utc - alarm_dt_utc
|
|
346
|
+
if alarm_dt_utc is not None
|
|
347
|
+
else now_local - alarm_dt_local
|
|
348
|
+
)
|
|
329
349
|
|
|
330
350
|
seconds = float(delta.total_seconds())
|
|
331
351
|
if seconds < 0:
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyezvizapi
|
|
3
|
+
Version: 1.0.3.8
|
|
4
|
+
Summary: EZVIZ API client for Home Assistant and CLI
|
|
5
|
+
Home-page: https://github.com/RenierM26/pyEzvizApi/
|
|
6
|
+
Author: Renier Moorcroft
|
|
7
|
+
Author-email: RenierM26@users.github.com
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
License-File: LICENSE.md
|
|
12
|
+
Requires-Dist: requests
|
|
13
|
+
Requires-Dist: aiohttp
|
|
14
|
+
Requires-Dist: xmltodict
|
|
15
|
+
Requires-Dist: pycryptodome
|
|
16
|
+
Requires-Dist: paho-mqtt
|
|
17
|
+
Requires-Dist: pandas
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: ruff; extra == "dev"
|
|
20
|
+
Requires-Dist: mypy; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest; extra == "dev"
|
|
22
|
+
Requires-Dist: types-requests; extra == "dev"
|
|
23
|
+
Dynamic: author
|
|
24
|
+
Dynamic: author-email
|
|
25
|
+
Dynamic: home-page
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
Dynamic: requires-python
|
|
28
|
+
|
|
29
|
+
# Ezviz PyPi
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
## Overview
|
|
34
|
+
|
|
35
|
+
Pilot your Ezviz cameras (and light bulbs) with this module. It is used by:
|
|
36
|
+
|
|
37
|
+
- The official Ezviz integration in Home Assistant
|
|
38
|
+
- The EZVIZ (Beta) custom integration for Home Assistant
|
|
39
|
+
|
|
40
|
+
You can also use it directly from the command line for quick checks and scripting.
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- Inspect device and connection status in table or JSON form
|
|
45
|
+
- Control cameras: PTZ, privacy/sleep/audio/IR/state LEDs, alarm settings
|
|
46
|
+
- Control light bulbs: toggle, status, brightness and color temperature
|
|
47
|
+
- Dump raw pagelist and device infos JSON for exploration/debugging
|
|
48
|
+
- Reuse a saved session token (no credentials needed after first login)
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
From PyPI:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install pyezvizapi
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
After installation, a `pyezvizapi` command is available on your PATH.
|
|
59
|
+
|
|
60
|
+
### Dependencies (development/local usage)
|
|
61
|
+
|
|
62
|
+
If you are running from a clone of this repository or using the helper scripts directly, ensure these packages are available:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install requests paho-mqtt pycryptodome pandas
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Quick Start
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# See available commands and options
|
|
72
|
+
pyezvizapi --help
|
|
73
|
+
|
|
74
|
+
# First-time login and save token for reuse
|
|
75
|
+
pyezvizapi -u YOUR_EZVIZ_USERNAME -p YOUR_EZVIZ_PASSWORD --save-token devices status
|
|
76
|
+
|
|
77
|
+
# Subsequent runs can reuse the saved token (no credentials needed)
|
|
78
|
+
pyezvizapi devices status --json
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## CLI Authentication
|
|
82
|
+
|
|
83
|
+
- Username/password: `-u/--username` and `-p/--password`
|
|
84
|
+
- Token file: `--token-file` (defaults to `ezviz_token.json` in the current directory)
|
|
85
|
+
- Save token: `--save-token` writes the current token after login
|
|
86
|
+
- MFA: The CLI prompts for a code if required by your account
|
|
87
|
+
- Region: `-r/--region` overrides the default region (`apiieu.ezvizlife.com`)
|
|
88
|
+
|
|
89
|
+
Examples:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# First-time login and save token locally
|
|
93
|
+
pyezvizapi -u YOUR_EZVIZ_USERNAME -p YOUR_EZVIZ_PASSWORD --save-token devices status
|
|
94
|
+
|
|
95
|
+
# Reuse saved token (no credentials)
|
|
96
|
+
pyezvizapi devices status --json
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Output Modes
|
|
100
|
+
|
|
101
|
+
- Default: human-readable tables (for list/status views)
|
|
102
|
+
- JSON: add `--json` for easy parsing and editor-friendly exploration
|
|
103
|
+
|
|
104
|
+
## CLI Commands
|
|
105
|
+
|
|
106
|
+
All commands are subcommands of the module runner:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pyezvizapi <command> [options]
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### devices
|
|
113
|
+
|
|
114
|
+
- Actions: `device`, `status`, `switch`, `connection`
|
|
115
|
+
- Examples:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Table view
|
|
119
|
+
pyezvizapi devices status
|
|
120
|
+
|
|
121
|
+
# JSON view
|
|
122
|
+
pyezvizapi devices status --json
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Sample table columns include:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
name | status | device_category | device_sub_category | sleep | privacy | audio | ir_led | state_led | local_ip | local_rtsp_port | battery_level | alarm_schedules_enabled | alarm_notify | Motion_Trigger
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The CLI also computes a `switch_flags` map for each device (all switch states by name, e.g. `privacy`, `sleep`, `sound`, `infrared_light`, `light`, etc.).
|
|
132
|
+
|
|
133
|
+
### camera
|
|
134
|
+
|
|
135
|
+
Requires `--serial`.
|
|
136
|
+
|
|
137
|
+
- Actions: `status`, `move`, `move_coords`, `unlock-door`, `unlock-gate`, `switch`, `alarm`, `select`
|
|
138
|
+
- Examples:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# Camera status
|
|
142
|
+
pyezvizapi camera --serial ABC123 status
|
|
143
|
+
|
|
144
|
+
# PTZ move
|
|
145
|
+
pyezvizapi camera --serial ABC123 move --direction up --speed 5
|
|
146
|
+
|
|
147
|
+
# Move by coordinates
|
|
148
|
+
pyezvizapi camera --serial ABC123 move_coords --x 0.4 --y 0.6
|
|
149
|
+
|
|
150
|
+
# Switch setters
|
|
151
|
+
pyezvizapi camera --serial ABC123 switch --switch privacy --enable 1
|
|
152
|
+
|
|
153
|
+
# Alarm settings (push notify, sound level, do-not-disturb)
|
|
154
|
+
pyezvizapi camera --serial ABC123 alarm --notify 1 --sound 2 --do_not_disturb 0
|
|
155
|
+
|
|
156
|
+
# Battery camera work mode
|
|
157
|
+
pyezvizapi camera --serial ABC123 select --battery_work_mode POWER_SAVE
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### devices_light
|
|
161
|
+
|
|
162
|
+
- Actions: `status`
|
|
163
|
+
- Example:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
pyezvizapi devices_light status
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### home_defence_mode
|
|
170
|
+
|
|
171
|
+
Set global defence mode for the account/home.
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
pyezvizapi home_defence_mode --mode HOME_MODE
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### mqtt
|
|
178
|
+
|
|
179
|
+
Connect to Ezviz MQTT push notifications using the current session token. Use `--debug` to see connection details.
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
pyezvizapi mqtt
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### MQTT push test script (standalone)
|
|
186
|
+
|
|
187
|
+
For quick experimentation, a small helper script is included which can use a saved token file or prompt for credentials with MFA and save the session token:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# With a previously saved token file
|
|
191
|
+
python config/custom_components/ezviz_cloud/pyezvizapi/test_mqtt.py --token-file ezviz_token.json
|
|
192
|
+
|
|
193
|
+
# Interactive login, then save token for next time
|
|
194
|
+
python config/custom_components/ezviz_cloud/pyezvizapi/test_mqtt.py --save-token
|
|
195
|
+
|
|
196
|
+
# Explicit credentials (not recommended for shared terminals)
|
|
197
|
+
python config/custom_components/ezviz_cloud/pyezvizapi/test_mqtt.py -u USER -p PASS --save-token
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### pagelist
|
|
201
|
+
|
|
202
|
+
Dump the complete raw pagelist JSON. Great for exploring unknown fields in an editor (e.g. Notepad++).
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
pyezvizapi pagelist > pagelist.json
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### device_infos
|
|
209
|
+
|
|
210
|
+
Dump the processed device infos mapping (what the integration consumes). Optionally filter to one serial:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# All devices
|
|
214
|
+
pyezvizapi device_infos > device_infos.json
|
|
215
|
+
|
|
216
|
+
# Single device
|
|
217
|
+
pyezvizapi device_infos --serial ABC123 > ABC123.json
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Remote door and gate unlock (CS-HPD7)
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
pyezvizapi camera --serial BAXXXXXXX-BAYYYYYYY unlock-door
|
|
224
|
+
pyezvizapi camera --serial BAXXXXXXX-BAYYYYYYY unlock-gate
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## RTSP authentication test (Basic → Digest)
|
|
228
|
+
|
|
229
|
+
Validate RTSP credentials by issuing a DESCRIBE request. Falls back from Basic to Digest auth automatically.
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
python -c "from config.custom_components.ezviz_cloud.pyezvizapi.test_cam_rtsp import TestRTSPAuth as T; T('<IP>', '<USER>', '<PASS>', '/Streaming/Channels/101').main()"
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
On success, the script prints a confirmation. On failure it raises one of:
|
|
236
|
+
|
|
237
|
+
- `InvalidHost`: Hostname/IP or port issue
|
|
238
|
+
- `AuthTestResultFailed`: Invalid credentials
|
|
239
|
+
|
|
240
|
+
## Development
|
|
241
|
+
|
|
242
|
+
Please format with Ruff and check typing with mypy.
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
ruff check .
|
|
246
|
+
mypy config/custom_components/ezviz_cloud/pyezvizapi
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Run style fixes where possible:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
ruff check --fix config/custom_components/ezviz_cloud/pyezvizapi
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Run tests with tox:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
tox
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Side Notes
|
|
262
|
+
|
|
263
|
+
There is no official API documentation. Much of this is based on reverse-engineering the Ezviz mobile app (Android/iOS). Some regions operate on separate endpoints; US example: `apiius.ezvizlife.com`.
|
|
264
|
+
|
|
265
|
+
Example:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
pyezvizapi -u username@domain.com -p PASS@123 -r apius.ezvizlife.com devices status
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
For advanced troubleshooting or new feature research, MITM proxy tools like mitmproxy/Charles/Fiddler can be used to inspect traffic from the app (see community guides for SSL unpinning and WSA usage).
|
|
272
|
+
|
|
273
|
+
## Contributing
|
|
274
|
+
|
|
275
|
+
Contributions are welcome — the API surface is large and there are many improvements possible.
|
|
276
|
+
|
|
277
|
+
## Versioning
|
|
278
|
+
|
|
279
|
+
We follow SemVer when publishing the library. See repository tags for released versions.
|
|
280
|
+
|
|
281
|
+
## License
|
|
282
|
+
|
|
283
|
+
Apache 2.0 — see `LICENSE.md`.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
Draft versions: 0.0.x
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
pyezvizapi/__init__.py,sha256=-OxHqxA9h0JZQW__GoZd1aByGquIHawCnNdeNlIg2Qo,3382
|
|
2
|
+
pyezvizapi/__main__.py,sha256=6vFvkh8gCD-Mo4CXd1deZfDeaaHR-Kc8pOu9vkUjQf4,20878
|
|
3
|
+
pyezvizapi/api_endpoints.py,sha256=2M5Vs4YB1VWZGcowT-4Fj2hhRNjFh976LT3jtRrqvrc,5754
|
|
4
|
+
pyezvizapi/camera.py,sha256=Pl5oIEdrFcv1Hz5sQI1IyyJIDCMjOjQdtExgKzmLoK8,22102
|
|
5
|
+
pyezvizapi/cas.py,sha256=3zHe-_a0KchCmGeAj1of-pV6oMPRUmSCIiDqBFsTK8A,6025
|
|
6
|
+
pyezvizapi/client.py,sha256=CKSK7hdQFmJAD0p1GPgEaqw7QAtqyhqLN8soR_VmSeA,142146
|
|
7
|
+
pyezvizapi/constants.py,sha256=6-AV7BvQPOQkSXrlrdOhnixDEF3eWiectV5jm5DtRSc,13115
|
|
8
|
+
pyezvizapi/exceptions.py,sha256=8rmxEUQdrziqMe-M1SeeRd0HtP2IDQ2xpJVj7wvOQyo,976
|
|
9
|
+
pyezvizapi/feature.py,sha256=m07s-6aEg0NijwjWZW4EAb23Rrr4RSRaBrYGQlqVwH0,16153
|
|
10
|
+
pyezvizapi/light_bulb.py,sha256=7kuOJmKsmAmE6KGJaUScjrRSTic8IhuToYrMRM-Y76s,7795
|
|
11
|
+
pyezvizapi/models.py,sha256=NQzwTP0yEe2IWU-Vc6nAn87xulpTuo0MX2Rcf0WxifA,4176
|
|
12
|
+
pyezvizapi/mqtt.py,sha256=JGjO-uXdKtLidYN1wEZ_bxEKIlNLmnB82Ziiac6oxWs,22373
|
|
13
|
+
pyezvizapi/test_cam_rtsp.py,sha256=pbuanoKs_Pryt2f5QctHIngzJG1nD6kv8nulQYh2yPc,5162
|
|
14
|
+
pyezvizapi/test_mqtt.py,sha256=gBaurvo2bu-7sOe14AqNouepJHI-tPWzC3WTpDQbvHM,4155
|
|
15
|
+
pyezvizapi/utils.py,sha256=WHGqI0bIy-G4yX6Vs7i_UDrr7dndx3yzbF_z7rDYGKE,13213
|
|
16
|
+
pyezvizapi-1.0.3.8.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
17
|
+
pyezvizapi-1.0.3.8.dist-info/licenses/LICENSE.md,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
18
|
+
pyezvizapi-1.0.3.8.dist-info/METADATA,sha256=8H30gnNHkm2Yg0zmBL_rx5CMZ6mft0zvBMgzBDbFKQo,7609
|
|
19
|
+
pyezvizapi-1.0.3.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
20
|
+
pyezvizapi-1.0.3.8.dist-info/top_level.txt,sha256=gMZTelIi8z7pXyTCQLLaIkxVRrDQ_lS2NEv0WgfHrHs,11
|
|
21
|
+
pyezvizapi-1.0.3.8.dist-info/RECORD,,
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: pyezvizapi
|
|
3
|
-
Version: 1.0.3.6
|
|
4
|
-
Summary: Pilot your Ezviz cameras
|
|
5
|
-
Home-page: https://github.com/RenierM26/pyEzvizApi/
|
|
6
|
-
Author: Renier Moorcroft
|
|
7
|
-
Author-email: RenierM26@users.github.com
|
|
8
|
-
License: Apache Software License 2.0
|
|
9
|
-
Requires-Python: >=3.11
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
License-File: LICENSE.md
|
|
12
|
-
Requires-Dist: requests
|
|
13
|
-
Requires-Dist: pandas
|
|
14
|
-
Requires-Dist: paho-mqtt
|
|
15
|
-
Requires-Dist: xmltodict
|
|
16
|
-
Requires-Dist: pycryptodome
|
|
17
|
-
Dynamic: author
|
|
18
|
-
Dynamic: author-email
|
|
19
|
-
Dynamic: description
|
|
20
|
-
Dynamic: home-page
|
|
21
|
-
Dynamic: license
|
|
22
|
-
Dynamic: license-file
|
|
23
|
-
Dynamic: requires-dist
|
|
24
|
-
Dynamic: requires-python
|
|
25
|
-
Dynamic: summary
|
|
26
|
-
|
|
27
|
-
Pilot your Ezviz cameras with this module. Please view readme on github
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
pyezvizapi/__init__.py,sha256=-OxHqxA9h0JZQW__GoZd1aByGquIHawCnNdeNlIg2Qo,3382
|
|
2
|
-
pyezvizapi/__main__.py,sha256=9uttTuOfO22tzyomJIV8ebFJ-G-YUNDYOadZ_0AgdNA,20925
|
|
3
|
-
pyezvizapi/api_endpoints.py,sha256=2M5Vs4YB1VWZGcowT-4Fj2hhRNjFh976LT3jtRrqvrc,5754
|
|
4
|
-
pyezvizapi/camera.py,sha256=Pl5oIEdrFcv1Hz5sQI1IyyJIDCMjOjQdtExgKzmLoK8,22102
|
|
5
|
-
pyezvizapi/cas.py,sha256=3zHe-_a0KchCmGeAj1of-pV6oMPRUmSCIiDqBFsTK8A,6025
|
|
6
|
-
pyezvizapi/client.py,sha256=LroaR4h_wYu8HB74l4_X6Vjq7rTiL88Idf18elUBj4A,141851
|
|
7
|
-
pyezvizapi/constants.py,sha256=5AxJYfof6NvebBcFvPkoKI6xinpkwmCnaauUvhvBMDY,12810
|
|
8
|
-
pyezvizapi/exceptions.py,sha256=8rmxEUQdrziqMe-M1SeeRd0HtP2IDQ2xpJVj7wvOQyo,976
|
|
9
|
-
pyezvizapi/feature.py,sha256=inqqk14Jgo3HOzml2GRUgz-hxbne4kT5iJ3L6SJhU8s,15990
|
|
10
|
-
pyezvizapi/light_bulb.py,sha256=9wgycG3dTvBbrsxQjQnXal-GA8VXPsIN1m-CTtRh8i0,7797
|
|
11
|
-
pyezvizapi/models.py,sha256=NQzwTP0yEe2IWU-Vc6nAn87xulpTuo0MX2Rcf0WxifA,4176
|
|
12
|
-
pyezvizapi/mqtt.py,sha256=aOL-gexZgYvCCaNQ03M4vZan91d5p2Fl_qsFykn9NW4,22365
|
|
13
|
-
pyezvizapi/test_cam_rtsp.py,sha256=O9NHh-vcNFfnzNw8jbuhM9a_5TWfNZIMXaJP7Lmkaj4,5162
|
|
14
|
-
pyezvizapi/test_mqtt.py,sha256=Orn-fwZPJIE4G5KROMX0MRAkLwU6nLb9LUtXyb2ZCQs,4147
|
|
15
|
-
pyezvizapi/utils.py,sha256=ozGncEyaIJJ8VYw8f-xfM2OBmqR8eNYLq728FFvbvr8,12757
|
|
16
|
-
pyezvizapi-1.0.3.6.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
17
|
-
pyezvizapi-1.0.3.6.dist-info/licenses/LICENSE.md,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
18
|
-
pyezvizapi-1.0.3.6.dist-info/METADATA,sha256=jp0jUC6j-BihBfzY79Qo_bdmk_yT6Db5wiJdlTHolmQ,695
|
|
19
|
-
pyezvizapi-1.0.3.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
20
|
-
pyezvizapi-1.0.3.6.dist-info/entry_points.txt,sha256=_BSJ3eNb2H_AZkRdsv1s4mojqWn3N7m503ujvg1SudA,56
|
|
21
|
-
pyezvizapi-1.0.3.6.dist-info/top_level.txt,sha256=gMZTelIi8z7pXyTCQLLaIkxVRrDQ_lS2NEv0WgfHrHs,11
|
|
22
|
-
pyezvizapi-1.0.3.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|