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 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._get_page_list() # noqa: SLF001
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, cast(dict[str, Any], client._token)) # noqa: SLF001
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
- FEATURE_CODE = generate_unique_code()
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
- feature = camera_data.get("FEATURE_INFO")
15
- if not isinstance(feature, Mapping):
16
- return {}
17
-
18
- for group in feature.values():
19
- if isinstance(group, Mapping):
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 entry in value:
196
- entry = decode_json(entry)
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
- obj_id = id(obj)
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
- elif isinstance(obj, Iterable):
259
- for item in obj:
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
- if isinstance(display_mode, Mapping):
322
- mode = display_mode.get("mode")
323
- else:
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 1100. Returns True on success.
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 commaseparated ``ext`` payload from EZVIZ.
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 Exception as err: # noqa: BLE001
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()
@@ -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
- print(describe)
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
- print(f"Basic auth result: {decoded}")
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
- print(describe)
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
- print(f"Digest auth result: {decoded}")
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
- print("Basic Auth test passed. Credentials Valid!")
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
- print("📩 New MQTT message:", msg)
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
- logging.getLogger(__name__).warning("Failed to read token file: %s", p)
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
- logging.getLogger(__name__).info("Saved token to %s", p)
54
+ _LOGGER.info("Saved token to %s", p)
54
55
  except OSError:
55
- logging.getLogger(__name__).warning("Failed to save token file: %s", p)
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
- print("No token found. Please enter Ezviz credentials.")
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
- print(f"Login failed: {exp}")
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
- print("Listening for MQTT messages... (Ctrl+C to quit)")
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
- print("\nStopping...")
123
+ _LOGGER.info("Stopping listener (keyboard interrupt)")
123
124
  finally:
124
125
  mqtt_client.stop()
125
- print("Stopped.")
126
+ _LOGGER.info("Listener stopped")
126
127
 
127
128
  if args.save_token and args.token_file:
128
- _save_token_file(args.token_file, cast(dict[str, Any], client._token)) # noqa: SLF001
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
- if separator in data:
67
- try:
68
- # Attempt to convert the string into a list
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
- except AttributeError:
72
- return data
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
- try:
92
- for key in keys:
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
- if input_data[:16] != b"hikencodepicture":
120
- _LOGGER.debug("Image header doesn't contain 'hikencodepicture'")
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 hikencodepicture + hash
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 > 1e11: # milliseconds
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
- if alarm_dt_utc is not None:
326
- delta = now_utc - alarm_dt_utc
327
- else:
328
- delta = now_local - alarm_dt_local
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
+ ![Upload Python Package](https://github.com/RenierM26/pyEzvizApi/workflows/Upload%20Python%20Package/badge.svg)
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,,
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- pyezvizapi = pyezvizapi.__main__:main