pyezvizapi 1.0.2.3__py3-none-any.whl → 1.0.4.3__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.
pyezvizapi/utils.py CHANGED
@@ -2,22 +2,49 @@
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__)
19
20
 
20
21
 
22
+ def coerce_int(value: Any) -> int | None:
23
+ """Best-effort coercion to int for mixed payloads."""
24
+
25
+ if isinstance(value, bool):
26
+ return int(value)
27
+
28
+ if isinstance(value, (int, float)):
29
+ return int(value)
30
+
31
+ try:
32
+ return int(str(value))
33
+ except (TypeError, ValueError):
34
+ return None
35
+
36
+
37
+ def decode_json(value: Any) -> Any:
38
+ """Decode a JSON string when possible, otherwise return the original value."""
39
+
40
+ if isinstance(value, str):
41
+ try:
42
+ return json.loads(value)
43
+ except (TypeError, ValueError):
44
+ return None
45
+ return value
46
+
47
+
21
48
  def convert_to_dict(data: Any) -> Any:
22
49
  """Recursively convert a string representation of a dictionary to a dictionary."""
23
50
  if isinstance(data, dict):
@@ -36,18 +63,64 @@ def convert_to_dict(data: Any) -> Any:
36
63
 
37
64
  def string_to_list(data: Any, separator: str = ",") -> Any:
38
65
  """Convert a string representation of a list to a list."""
39
- if isinstance(data, str):
40
- if separator in data:
41
- try:
42
- # Attempt to convert the string into a list
43
- 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)
44
70
 
45
- except AttributeError:
46
- return data
71
+ except AttributeError:
72
+ return data
47
73
 
48
74
  return data
49
75
 
50
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
+
51
124
  def fetch_nested_value(data: Any, keys: list, default_value: Any = None) -> Any:
52
125
  """Fetch the value corresponding to the given nested keys in a dictionary.
53
126
 
@@ -62,14 +135,8 @@ def fetch_nested_value(data: Any, keys: list, default_value: Any = None) -> Any:
62
135
  The value corresponding to the nested keys or the default value.
63
136
 
64
137
  """
65
- try:
66
- for key in keys:
67
- data = data[key]
68
-
69
- except (KeyError, TypeError):
70
- return default_value
71
-
72
- return data
138
+ value = first_nested(data, keys, _MISSING)
139
+ return default_value if value is _MISSING else value
73
140
 
74
141
 
75
142
  def decrypt_image(input_data: bytes, password: str) -> bytes:
@@ -86,37 +153,102 @@ def decrypt_image(input_data: bytes, password: str) -> bytes:
86
153
  bytes: Decrypted image data
87
154
 
88
155
  """
89
- if len(input_data) < 48:
156
+ header_len = len(HIK_ENCRYPTION_HEADER)
157
+ min_length = header_len + 32 # header + md5 hash
158
+
159
+ if len(input_data) < min_length:
90
160
  raise PyEzvizError("Invalid image data")
91
161
 
92
- # check header
93
- if input_data[:16] != b"hikencodepicture":
94
- _LOGGER.debug("Image header doesn't contain 'hikencodepicture'")
162
+ header_index = input_data.find(HIK_ENCRYPTION_HEADER)
163
+ if header_index == -1:
164
+ _LOGGER.debug("Image header doesn't contain %s", HIK_ENCRYPTION_HEADER)
95
165
  return input_data
96
166
 
97
- file_hash = input_data[16:48]
167
+ if header_index:
168
+ _LOGGER.debug("Image header found at offset %s, trimming preamble", header_index)
169
+ input_data = input_data[header_index:]
170
+ if len(input_data) < min_length:
171
+ raise PyEzvizError("Invalid image data after trimming preamble")
172
+
173
+ hash_end = header_len + 32
174
+ blocks = _split_encrypted_blocks(input_data, header_len, min_length)
175
+ if not blocks:
176
+ raise PyEzvizError("Invalid image data")
177
+
178
+ decrypted_parts = [
179
+ _decrypt_single_block(block, password, header_len, hash_end) for block in blocks
180
+ ]
181
+ if len(decrypted_parts) > 1:
182
+ _LOGGER.debug("Decrypted %s concatenated image blocks", len(decrypted_parts))
183
+ return b"".join(decrypted_parts)
184
+
185
+
186
+ def _split_encrypted_blocks(
187
+ data: bytes, header_len: int, min_length: int
188
+ ) -> list[bytes]:
189
+ """Split concatenated hikencodepicture segments into individual blocks."""
190
+ blocks: list[bytes] = []
191
+ cursor = 0
192
+ data_len = len(data)
193
+
194
+ while cursor <= data_len - min_length:
195
+ if data[cursor : cursor + header_len] != HIK_ENCRYPTION_HEADER:
196
+ next_header = data.find(HIK_ENCRYPTION_HEADER, cursor + 1)
197
+ if next_header == -1:
198
+ break
199
+ cursor = next_header
200
+ continue
201
+
202
+ next_header = data.find(HIK_ENCRYPTION_HEADER, cursor + header_len)
203
+ block = data[cursor : next_header if next_header != -1 else data_len]
204
+ if len(block) < min_length:
205
+ break
206
+ blocks.append(block)
207
+ if next_header == -1:
208
+ break
209
+ cursor = next_header
210
+
211
+ return blocks
212
+
213
+
214
+ def _decrypt_single_block(
215
+ block: bytes, password: str, header_len: int, hash_end: int
216
+ ) -> bytes:
217
+ """Decrypt a single hikencodepicture block."""
218
+ file_hash = block[header_len:hash_end]
98
219
  passwd_hash = md5(str.encode(md5(str.encode(password)).hexdigest())).hexdigest()
99
220
  if file_hash != str.encode(passwd_hash):
100
221
  raise PyEzvizError("Invalid password")
101
222
 
223
+ ciphertext = block[hash_end:]
224
+ if not ciphertext:
225
+ raise PyEzvizError("Missing ciphertext payload")
226
+
227
+ remainder = len(ciphertext) % AES.block_size
228
+ if remainder:
229
+ _LOGGER.debug(
230
+ "Ciphertext not aligned to 16 bytes; trimming %s trailing bytes", remainder
231
+ )
232
+ ciphertext = ciphertext[:-remainder]
233
+ if not ciphertext:
234
+ raise PyEzvizError("Ciphertext too short after alignment adjustment")
235
+
102
236
  key = str.encode(password.ljust(16, "\u0000")[:16])
103
237
  iv_code = bytes([48, 49, 50, 51, 52, 53, 54, 55, 0, 0, 0, 0, 0, 0, 0, 0])
104
238
  cipher = AES.new(key, AES.MODE_CBC, iv_code)
105
239
 
106
- next_chunk = b""
107
- output_data = b""
108
- finished = False
109
- i = 48 # offset hikencodepicture + hash
110
240
  chunk_size = 1024 * AES.block_size
111
- while not finished:
112
- chunk, next_chunk = next_chunk, cipher.decrypt(input_data[i : i + chunk_size])
113
- if len(next_chunk) == 0:
114
- padding_length = chunk[-1]
115
- chunk = chunk[:-padding_length]
116
- finished = True
117
- output_data += chunk
118
- i += chunk_size
119
- return output_data
241
+ output_data = bytearray()
242
+
243
+ for start in range(0, len(ciphertext), chunk_size):
244
+ block_chunk = cipher.decrypt(ciphertext[start : start + chunk_size])
245
+ if start + chunk_size >= len(ciphertext):
246
+ padding_length = block_chunk[-1]
247
+ block_chunk = block_chunk[:-padding_length]
248
+ output_data.extend(block_chunk)
249
+
250
+ return bytes(output_data)
251
+
120
252
 
121
253
 
122
254
  def return_password_hash(password: str) -> str:
@@ -169,34 +301,11 @@ def deep_merge(dict1: Any, dict2: Any) -> Any:
169
301
  return merged
170
302
 
171
303
 
172
- def generate_unique_code() -> str:
173
- """Generate a deterministic, platform-agnostic unique code for the current host.
174
-
175
- This function retrieves the host's MAC address using Python's standard
176
- `uuid.getnode()` (works on Windows, Linux, macOS), converts it to a
177
- canonical string representation, and then hashes it using MD5 to produce
178
- a fixed-length hexadecimal string.
179
-
180
- Returns:
181
- str: A 32-character hexadecimal string uniquely representing
182
- the host's MAC address. For example:
183
- 'a94e6756hghjgfghg49e0f310d9e44a'.
184
-
185
- Notes:
186
- - The output is deterministic: the same machine returns the same code.
187
- - If the MAC address changes (e.g., different network adapter),
188
- the output will change.
189
- - MD5 is used here only for ID generation, not for security.
190
- """
191
- mac_int = uuid.getnode()
192
- mac_str = ":".join(f"{(mac_int >> i) & 0xFF:02x}" for i in range(40, -1, -8))
193
- return md5(mac_str.encode("utf-8")).hexdigest()
194
-
195
-
196
304
  # ---------------------------------------------------------------------------
197
305
  # Time helpers for alarm/motion handling
198
306
  # ---------------------------------------------------------------------------
199
307
 
308
+
200
309
  def normalize_alarm_time(
201
310
  last_alarm: dict[str, Any], tzinfo: datetime.tzinfo
202
311
  ) -> tuple[datetime.datetime | None, datetime.datetime | None, str | None]:
@@ -228,7 +337,7 @@ def normalize_alarm_time(
228
337
  if epoch is not None:
229
338
  try:
230
339
  ts = float(epoch if not isinstance(epoch, str) else float(epoch))
231
- if ts > 1e11: # milliseconds
340
+ if ts > MILLISECONDS_THRESHOLD: # milliseconds
232
341
  ts /= 1000.0
233
342
  event_utc = datetime.datetime.fromtimestamp(ts, tz=datetime.UTC)
234
343
  alarm_dt_local = event_utc.astimezone(tzinfo)
@@ -241,14 +350,15 @@ def normalize_alarm_time(
241
350
  raw_norm, "%Y-%m-%d %H:%M:%S"
242
351
  ).replace(tzinfo=tzinfo)
243
352
  diff = abs(
244
- (event_utc - dt_str_local.astimezone(datetime.UTC)).total_seconds()
353
+ (
354
+ event_utc - dt_str_local.astimezone(datetime.UTC)
355
+ ).total_seconds()
245
356
  )
246
357
  if diff > 120:
247
358
  # Reinterpret epoch as local clock time in camera tz
248
- naive_utc = (
249
- datetime.datetime.fromtimestamp(ts, tz=datetime.UTC)
250
- .replace(tzinfo=None)
251
- )
359
+ naive_utc = datetime.datetime.fromtimestamp(
360
+ ts, tz=datetime.UTC
361
+ ).replace(tzinfo=None)
252
362
  event_local_reint = naive_utc.replace(tzinfo=tzinfo)
253
363
  alarm_dt_local = event_local_reint
254
364
  alarm_dt_utc = event_local_reint.astimezone(datetime.UTC)
@@ -266,9 +376,9 @@ def normalize_alarm_time(
266
376
  if raw_time_str:
267
377
  raw = raw_time_str.replace("Today", str(now_local.date()))
268
378
  try:
269
- alarm_dt_local = datetime.datetime.strptime(raw, "%Y-%m-%d %H:%M:%S").replace(
270
- tzinfo=tzinfo
271
- )
379
+ alarm_dt_local = datetime.datetime.strptime(
380
+ raw, "%Y-%m-%d %H:%M:%S"
381
+ ).replace(tzinfo=tzinfo)
272
382
  alarm_dt_utc = alarm_dt_local.astimezone(datetime.UTC)
273
383
  alarm_str = alarm_dt_local.strftime("%Y-%m-%d %H:%M:%S")
274
384
  except ValueError:
@@ -294,10 +404,11 @@ def compute_motion_from_alarm(
294
404
  now_local = datetime.datetime.now(tz=tzinfo).replace(microsecond=0)
295
405
  now_utc = datetime.datetime.now(tz=datetime.UTC).replace(microsecond=0)
296
406
 
297
- if alarm_dt_utc is not None:
298
- delta = now_utc - alarm_dt_utc
299
- else:
300
- delta = now_local - alarm_dt_local
407
+ delta = (
408
+ now_utc - alarm_dt_utc
409
+ if alarm_dt_utc is not None
410
+ else now_local - alarm_dt_local
411
+ )
301
412
 
302
413
  seconds = float(delta.total_seconds())
303
414
  if seconds < 0:
@@ -0,0 +1,286 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyezvizapi
3
+ Version: 1.0.4.3
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: xmltodict
14
+ Requires-Dist: pycryptodome
15
+ Requires-Dist: paho-mqtt
16
+ Requires-Dist: pandas
17
+ Provides-Extra: dev
18
+ Requires-Dist: ruff; extra == "dev"
19
+ Requires-Dist: mypy; extra == "dev"
20
+ Requires-Dist: pytest; extra == "dev"
21
+ Requires-Dist: types-requests; extra == "dev"
22
+ Dynamic: author
23
+ Dynamic: author-email
24
+ Dynamic: home-page
25
+ Dynamic: license-file
26
+ Dynamic: requires-python
27
+
28
+ # Ezviz PyPi
29
+
30
+ ![Upload Python Package](https://github.com/RenierM26/pyEzvizApi/workflows/Upload%20Python%20Package/badge.svg)
31
+
32
+ ## Overview
33
+
34
+ Pilot your Ezviz cameras (and light bulbs) with this module. It is used by:
35
+
36
+ - The official Ezviz integration in Home Assistant
37
+ - The EZVIZ (Beta) custom integration for Home Assistant
38
+
39
+ You can also use it directly from the command line for quick checks and scripting.
40
+
41
+ ## Features
42
+
43
+ - Inspect device and connection status in table or JSON form
44
+ - Control cameras: PTZ, privacy/sleep/audio/IR/state LEDs, alarm settings
45
+ - Control light bulbs: toggle, status, brightness and color temperature
46
+ - Dump raw pagelist and device infos JSON for exploration/debugging
47
+ - Reuse a saved session token (no credentials needed after first login)
48
+
49
+ ## Install
50
+
51
+ From PyPI:
52
+
53
+ ```bash
54
+ pip install pyezvizapi
55
+ ```
56
+
57
+ After installation, a `pyezvizapi` command is available on your PATH.
58
+
59
+ ### Dependencies (development/local usage)
60
+
61
+ If you are running from a clone of this repository or using the helper scripts directly, ensure these packages are available:
62
+
63
+ ```bash
64
+ pip install requests paho-mqtt pycryptodome pandas
65
+ ```
66
+
67
+ ## Quick Start
68
+
69
+ ```bash
70
+ # See available commands and options
71
+ pyezvizapi --help
72
+
73
+ # First-time login and save token for reuse
74
+ pyezvizapi -u YOUR_EZVIZ_USERNAME -p YOUR_EZVIZ_PASSWORD --save-token devices status
75
+
76
+ # Subsequent runs can reuse the saved token (no credentials needed)
77
+ pyezvizapi devices status --json
78
+ ```
79
+
80
+ ## CLI Authentication
81
+
82
+ - Username/password: `-u/--username` and `-p/--password`
83
+ - Token file: `--token-file` (defaults to `ezviz_token.json` in the current directory)
84
+ - Save token: `--save-token` writes the current token after login
85
+ - MFA: The CLI prompts for a code if required by your account
86
+ - Region: `-r/--region` overrides the default region (`apiieu.ezvizlife.com`)
87
+
88
+ Examples:
89
+
90
+ ```bash
91
+ # First-time login and save token locally
92
+ pyezvizapi -u YOUR_EZVIZ_USERNAME -p YOUR_EZVIZ_PASSWORD --save-token devices status
93
+
94
+ # Reuse saved token (no credentials)
95
+ pyezvizapi devices status --json
96
+ ```
97
+
98
+ ## Output Modes
99
+
100
+ - Default: human-readable tables (for list/status views)
101
+ - JSON: add `--json` for easy parsing and editor-friendly exploration
102
+
103
+ ## CLI Commands
104
+
105
+ All commands are subcommands of the module runner:
106
+
107
+ ```bash
108
+ pyezvizapi <command> [options]
109
+ ```
110
+
111
+ ### devices
112
+
113
+ - Actions: `device`, `status`, `switch`, `connection`
114
+ - Examples:
115
+
116
+ ```bash
117
+ # Table view
118
+ pyezvizapi devices status
119
+
120
+ # JSON view
121
+ pyezvizapi devices status --json
122
+ ```
123
+
124
+ Sample table columns include:
125
+
126
+ ```
127
+ 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
128
+ ```
129
+
130
+ 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.).
131
+
132
+ ### camera
133
+
134
+ Requires `--serial`.
135
+
136
+ - Actions: `status`, `move`, `move_coords`, `unlock-door`, `unlock-gate`, `switch`, `alarm`, `select`
137
+ - Examples:
138
+
139
+ ```bash
140
+ # Camera status
141
+ pyezvizapi camera --serial ABC123 status
142
+
143
+ # PTZ move
144
+ pyezvizapi camera --serial ABC123 move --direction up --speed 5
145
+
146
+ # Move by coordinates
147
+ pyezvizapi camera --serial ABC123 move_coords --x 0.4 --y 0.6
148
+
149
+ # Switch setters
150
+ pyezvizapi camera --serial ABC123 switch --switch privacy --enable 1
151
+
152
+ # Alarm settings (push notify, sound level, do-not-disturb)
153
+ pyezvizapi camera --serial ABC123 alarm --notify 1 --sound 2 --do_not_disturb 0
154
+
155
+ # Battery camera work mode
156
+ pyezvizapi camera --serial ABC123 select --battery_work_mode POWER_SAVE
157
+ ```
158
+
159
+ ### devices_light
160
+
161
+ - Actions: `status`
162
+ - Example:
163
+
164
+ ```bash
165
+ pyezvizapi devices_light status
166
+ ```
167
+
168
+ ### home_defence_mode
169
+
170
+ Set global defence mode for the account/home.
171
+
172
+ ```bash
173
+ pyezvizapi home_defence_mode --mode HOME_MODE
174
+ ```
175
+
176
+ ### mqtt
177
+
178
+ Connect to Ezviz MQTT push notifications using the current session token. Use `--debug` to see connection details.
179
+
180
+ ```bash
181
+ pyezvizapi mqtt
182
+ ```
183
+
184
+ #### MQTT push test script (standalone)
185
+
186
+ 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:
187
+
188
+ ```bash
189
+ # With a previously saved token file
190
+ python config/custom_components/ezviz_cloud/pyezvizapi/test_mqtt.py --token-file ezviz_token.json
191
+
192
+ # Interactive login, then save token for next time
193
+ python config/custom_components/ezviz_cloud/pyezvizapi/test_mqtt.py --save-token
194
+
195
+ # Explicit credentials (not recommended for shared terminals)
196
+ python config/custom_components/ezviz_cloud/pyezvizapi/test_mqtt.py -u USER -p PASS --save-token
197
+ ```
198
+
199
+ ### pagelist
200
+
201
+ Dump the complete raw pagelist JSON. Great for exploring unknown fields in an editor (e.g. Notepad++).
202
+
203
+ ```bash
204
+ pyezvizapi pagelist > pagelist.json
205
+ ```
206
+
207
+ ### device_infos
208
+
209
+ Dump the processed device infos mapping (what the integration consumes). Optionally filter to one serial:
210
+
211
+ ```bash
212
+ # All devices
213
+ pyezvizapi device_infos > device_infos.json
214
+
215
+ # Single device
216
+ pyezvizapi device_infos --serial ABC123 > ABC123.json
217
+ ```
218
+
219
+ ## Remote door and gate unlock (CS-HPD7)
220
+
221
+ ```bash
222
+ pyezvizapi camera --serial BAXXXXXXX-BAYYYYYYY unlock-door
223
+ pyezvizapi camera --serial BAXXXXXXX-BAYYYYYYY unlock-gate
224
+ ```
225
+
226
+ ## RTSP authentication test (Basic → Digest)
227
+
228
+ Validate RTSP credentials by issuing a DESCRIBE request. Falls back from Basic to Digest auth automatically.
229
+
230
+ ```bash
231
+ python -c "from config.custom_components.ezviz_cloud.pyezvizapi.test_cam_rtsp import TestRTSPAuth as T; T('<IP>', '<USER>', '<PASS>', '/Streaming/Channels/101').main()"
232
+ ```
233
+
234
+ On success, the script prints a confirmation. On failure it raises one of:
235
+
236
+ - `InvalidHost`: Hostname/IP or port issue
237
+ - `AuthTestResultFailed`: Invalid credentials
238
+
239
+ ## Development
240
+
241
+ Please format with Ruff and check typing with mypy.
242
+
243
+ ```bash
244
+ ruff check .
245
+ mypy config/custom_components/ezviz_cloud/pyezvizapi
246
+ ```
247
+
248
+ Run style fixes where possible:
249
+
250
+ ```bash
251
+ ruff check --fix config/custom_components/ezviz_cloud/pyezvizapi
252
+ ```
253
+
254
+ Run tests with tox:
255
+
256
+ ```bash
257
+ tox
258
+ ```
259
+
260
+ ## Side Notes
261
+
262
+ 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`.
263
+
264
+ Example:
265
+
266
+ ```bash
267
+ pyezvizapi -u username@domain.com -p PASS@123 -r apius.ezvizlife.com devices status
268
+ ```
269
+
270
+ 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).
271
+
272
+ ## Contributing
273
+
274
+ Contributions are welcome — the API surface is large and there are many improvements possible.
275
+
276
+ ## Versioning
277
+
278
+ We follow SemVer when publishing the library. See repository tags for released versions.
279
+
280
+ ## License
281
+
282
+ Apache 2.0 — see `LICENSE.md`.
283
+
284
+ ---
285
+
286
+ Draft versions: 0.0.x
@@ -0,0 +1,21 @@
1
+ pyezvizapi/__init__.py,sha256=-OxHqxA9h0JZQW__GoZd1aByGquIHawCnNdeNlIg2Qo,3382
2
+ pyezvizapi/__main__.py,sha256=GmV-9mfXhbzZq3g--cpM6Tex8EX4DBHXE1jeDhD8UXs,24112
3
+ pyezvizapi/api_endpoints.py,sha256=NSUKafpmd_DaNtmlOsRplXeQB1PYjZ4roClt_BNnNyE,5802
4
+ pyezvizapi/camera.py,sha256=yHP4ENva6ep2BY85VvlyHvimCOCrHCwOCnCfpAC2mNY,28517
5
+ pyezvizapi/cas.py,sha256=3zHe-_a0KchCmGeAj1of-pV6oMPRUmSCIiDqBFsTK8A,6025
6
+ pyezvizapi/client.py,sha256=4c93Eg-0AQvxjxgBMX4dhi3ywOftxocr-4iIoM_HxRM,153968
7
+ pyezvizapi/constants.py,sha256=Ae838wTtHpOyNBnLQf56CFGHXpG9kPmgotjP9--J5V4,13570
8
+ pyezvizapi/exceptions.py,sha256=8rmxEUQdrziqMe-M1SeeRd0HtP2IDQ2xpJVj7wvOQyo,976
9
+ pyezvizapi/feature.py,sha256=8QeAo6vnIev_ovlDnVvnRbC39Xe6piLgBlFEuFE7qIA,16789
10
+ pyezvizapi/light_bulb.py,sha256=7kuOJmKsmAmE6KGJaUScjrRSTic8IhuToYrMRM-Y76s,7795
11
+ pyezvizapi/models.py,sha256=NQzwTP0yEe2IWU-Vc6nAn87xulpTuo0MX2Rcf0WxifA,4176
12
+ pyezvizapi/mqtt.py,sha256=uzEFT69LLCtP4paTSdCgMBkGAkuqRD02scfgqtK1bxU,22656
13
+ pyezvizapi/test_cam_rtsp.py,sha256=pbuanoKs_Pryt2f5QctHIngzJG1nD6kv8nulQYh2yPc,5162
14
+ pyezvizapi/test_mqtt.py,sha256=xUtyw5I3KBkMVGe2xhok7SWXFyZZMXrlMADoPo_5l_k,5744
15
+ pyezvizapi/utils.py,sha256=B-Lt4p14TTcJyrZvJ699GiUGj6qcd4vGmkKUf13bf_I,15455
16
+ pyezvizapi-1.0.4.3.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
17
+ pyezvizapi-1.0.4.3.dist-info/licenses/LICENSE.md,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
18
+ pyezvizapi-1.0.4.3.dist-info/METADATA,sha256=WWMQuriHh98Vj2f3uTsQhFdlC0PdHrp8l_zmP7eqcaQ,7586
19
+ pyezvizapi-1.0.4.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
+ pyezvizapi-1.0.4.3.dist-info/top_level.txt,sha256=gMZTelIi8z7pXyTCQLLaIkxVRrDQ_lS2NEv0WgfHrHs,11
21
+ pyezvizapi-1.0.4.3.dist-info/RECORD,,
@@ -1,27 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: pyezvizapi
3
- Version: 1.0.2.3
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