pyezvizapi 1.0.3.7__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/constants.py CHANGED
@@ -6,10 +6,19 @@ 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
pyezvizapi/feature.py CHANGED
@@ -188,8 +188,8 @@ def _normalize_port_list(value: Any) -> list[dict[str, Any]] | None:
188
188
  return None
189
189
 
190
190
  normalized: list[dict[str, Any]] = []
191
- for entry in value:
192
- entry = decode_json(entry)
191
+ for raw_entry in value:
192
+ entry = decode_json(raw_entry)
193
193
  if not isinstance(entry, Mapping):
194
194
  return None
195
195
  port = coerce_int(entry.get("portNo"))
@@ -205,57 +205,68 @@ def normalize_port_security(payload: Any) -> dict[str, Any]:
205
205
 
206
206
  seen: set[int] = set()
207
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
+
208
260
  def _walk(obj: Any, hint: bool | None = None) -> dict[str, Any] | None:
209
261
  obj = decode_json(obj)
210
262
  if obj is None:
211
263
  return None
212
264
 
213
265
  if isinstance(obj, Mapping):
214
- obj_id = id(obj)
215
- if obj_id in seen:
216
- return None
217
- seen.add(obj_id)
218
-
219
- enabled_local = obj.get("enabled")
220
- if isinstance(enabled_local, bool):
221
- hint = enabled_local
222
-
223
- ports = _normalize_port_list(obj.get("portSecurityList"))
224
- if ports is not None:
225
- return {
226
- "portSecurityList": ports,
227
- "enabled": bool(enabled_local)
228
- if isinstance(enabled_local, bool)
229
- else bool(hint)
230
- if isinstance(hint, bool)
231
- else True,
232
- }
233
-
234
- for key in (
235
- "PortSecurity",
236
- "value",
237
- "data",
238
- "NetworkSecurityProtection",
239
- ):
240
- if key in obj:
241
- candidate = _walk(obj[key], hint)
242
- if candidate:
243
- if "enabled" not in candidate and isinstance(hint, bool):
244
- candidate["enabled"] = hint
245
- return candidate
246
-
247
- for value in obj.values():
248
- candidate = _walk(value, hint)
249
- if candidate:
250
- if "enabled" not in candidate and isinstance(hint, bool):
251
- candidate["enabled"] = hint
252
- return candidate
266
+ return _walk_mapping(obj, hint)
253
267
 
254
- elif isinstance(obj, Iterable):
255
- for item in obj:
256
- candidate = _walk(item, hint)
257
- if candidate:
258
- return candidate
268
+ if isinstance(obj, Iterable) and not isinstance(obj, (str, bytes, bytearray)):
269
+ return _walk_iterable(obj, hint)
259
270
 
260
271
  return None
261
272
 
@@ -314,10 +325,9 @@ def display_mode_value(camera_data: Mapping[str, Any]) -> int:
314
325
  display_mode = optionals.get("display_mode")
315
326
  display_mode = decode_json(display_mode)
316
327
 
317
- if isinstance(display_mode, Mapping):
318
- mode = display_mode.get("mode")
319
- else:
320
- mode = display_mode
328
+ mode = (
329
+ display_mode.get("mode") if isinstance(display_mode, Mapping) else display_mode
330
+ )
321
331
 
322
332
  if isinstance(mode, int) and mode in (1, 2, 3):
323
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",
@@ -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,14 +116,14 @@ 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
129
  _save_token_file(args.token_file, client.export_token())
@@ -132,4 +133,3 @@ def main(argv: list[str] | None = None) -> int:
132
133
 
133
134
  if __name__ == "__main__":
134
135
  sys.exit(main())
135
- # ruff: noqa: T201
pyezvizapi/utils.py CHANGED
@@ -9,7 +9,6 @@ import json
9
9
  import logging
10
10
  import re as _re
11
11
  from typing import Any
12
- import uuid
13
12
  from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
14
13
 
15
14
  from Crypto.Cipher import AES
@@ -64,14 +63,13 @@ def convert_to_dict(data: Any) -> Any:
64
63
 
65
64
  def string_to_list(data: Any, separator: str = ",") -> Any:
66
65
  """Convert a string representation of a list to a list."""
67
- if isinstance(data, str):
68
- if separator in data:
69
- try:
70
- # Attempt to convert the string into a list
71
- 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)
72
70
 
73
- except AttributeError:
74
- return data
71
+ except AttributeError:
72
+ return data
75
73
 
76
74
  return data
77
75
 
@@ -79,6 +77,7 @@ def string_to_list(data: Any, separator: str = ",") -> Any:
79
77
  PathComponent = str | int
80
78
  WILDCARD_STEP = "*"
81
79
  _MISSING = object()
80
+ MILLISECONDS_THRESHOLD = 1e11
82
81
 
83
82
 
84
83
  def iter_nested(data: Any, path: Iterable[PathComponent]) -> Iterator[Any]:
@@ -100,9 +99,12 @@ def iter_nested(data: Any, path: Iterable[PathComponent]) -> Iterator[Any]:
100
99
  next_level.append(candidate[step])
101
100
  continue
102
101
 
103
- if isinstance(candidate, (list, tuple)) and isinstance(step, int):
104
- if -len(candidate) <= step < len(candidate):
105
- next_level.append(candidate[step])
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])
106
108
 
107
109
  current = next_level
108
110
  if not current:
@@ -236,30 +238,6 @@ def deep_merge(dict1: Any, dict2: Any) -> Any:
236
238
  return merged
237
239
 
238
240
 
239
- def generate_unique_code() -> str:
240
- """Generate a deterministic, platform-agnostic unique code for the current host.
241
-
242
- This function retrieves the host's MAC address using Python's standard
243
- `uuid.getnode()` (works on Windows, Linux, macOS), converts it to a
244
- canonical string representation, and then hashes it using MD5 to produce
245
- a fixed-length hexadecimal string.
246
-
247
- Returns:
248
- str: A 32-character hexadecimal string uniquely representing
249
- the host's MAC address. For example:
250
- 'a94e6756hghjgfghg49e0f310d9e44a'.
251
-
252
- Notes:
253
- - The output is deterministic: the same machine returns the same code.
254
- - If the MAC address changes (e.g., different network adapter),
255
- the output will change.
256
- - MD5 is used here only for ID generation, not for security.
257
- """
258
- mac_int = uuid.getnode()
259
- mac_str = ":".join(f"{(mac_int >> i) & 0xFF:02x}" for i in range(40, -1, -8))
260
- return md5(mac_str.encode("utf-8")).hexdigest()
261
-
262
-
263
241
  # ---------------------------------------------------------------------------
264
242
  # Time helpers for alarm/motion handling
265
243
  # ---------------------------------------------------------------------------
@@ -296,7 +274,7 @@ def normalize_alarm_time(
296
274
  if epoch is not None:
297
275
  try:
298
276
  ts = float(epoch if not isinstance(epoch, str) else float(epoch))
299
- if ts > 1e11: # milliseconds
277
+ if ts > MILLISECONDS_THRESHOLD: # milliseconds
300
278
  ts /= 1000.0
301
279
  event_utc = datetime.datetime.fromtimestamp(ts, tz=datetime.UTC)
302
280
  alarm_dt_local = event_utc.astimezone(tzinfo)
@@ -363,10 +341,11 @@ def compute_motion_from_alarm(
363
341
  now_local = datetime.datetime.now(tz=tzinfo).replace(microsecond=0)
364
342
  now_utc = datetime.datetime.now(tz=datetime.UTC).replace(microsecond=0)
365
343
 
366
- if alarm_dt_utc is not None:
367
- delta = now_utc - alarm_dt_utc
368
- else:
369
- 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
+ )
370
349
 
371
350
  seconds = float(delta.total_seconds())
372
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.7
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=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=4aoo2jKV88quLIPRwRAH0xW7yCuQfsU9tNtSxq7Xnnk,12854
8
- pyezvizapi/exceptions.py,sha256=8rmxEUQdrziqMe-M1SeeRd0HtP2IDQ2xpJVj7wvOQyo,976
9
- pyezvizapi/feature.py,sha256=TwQDAVFXqrpe5s0rXyJSqsHCnQsw1MNSTsUGhb61EzI,15886
10
- pyezvizapi/light_bulb.py,sha256=9wgycG3dTvBbrsxQjQnXal-GA8VXPsIN1m-CTtRh8i0,7797
11
- pyezvizapi/models.py,sha256=NQzwTP0yEe2IWU-Vc6nAn87xulpTuo0MX2Rcf0WxifA,4176
12
- pyezvizapi/mqtt.py,sha256=_JHklUsqqwrgKk3k8RyBgo3bp1utEOr5KSBe8Oz92qg,22375
13
- pyezvizapi/test_cam_rtsp.py,sha256=O9NHh-vcNFfnzNw8jbuhM9a_5TWfNZIMXaJP7Lmkaj4,5162
14
- pyezvizapi/test_mqtt.py,sha256=-0L621V4nzm6x0oLgurAgkJZopVZ5Os7n1hLVMh3qIc,4117
15
- pyezvizapi/utils.py,sha256=Gmn6Ljuo7SUSt1nFN-JmuoWAJGKqUYqur4SBESlhjKI,14153
16
- pyezvizapi-1.0.3.7.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
17
- pyezvizapi-1.0.3.7.dist-info/licenses/LICENSE.md,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
18
- pyezvizapi-1.0.3.7.dist-info/METADATA,sha256=UYwabH3pFMA4UryJZ8h9Pu-alu2TuG1ee-enZQ2j8bU,695
19
- pyezvizapi-1.0.3.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- pyezvizapi-1.0.3.7.dist-info/entry_points.txt,sha256=_BSJ3eNb2H_AZkRdsv1s4mojqWn3N7m503ujvg1SudA,56
21
- pyezvizapi-1.0.3.7.dist-info/top_level.txt,sha256=gMZTelIi8z7pXyTCQLLaIkxVRrDQ_lS2NEv0WgfHrHs,11
22
- pyezvizapi-1.0.3.7.dist-info/RECORD,,
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- pyezvizapi = pyezvizapi.__main__:main