pyezvizapi 1.0.2.2__py3-none-any.whl → 1.0.2.4__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 +25 -8
- pyezvizapi/camera.py +10 -139
- pyezvizapi/cas.py +4 -2
- pyezvizapi/client.py +59 -21
- pyezvizapi/constants.py +1 -1
- pyezvizapi/test_cam_rtsp.py +37 -13
- pyezvizapi/utils.py +158 -0
- {pyezvizapi-1.0.2.2.dist-info → pyezvizapi-1.0.2.4.dist-info}/METADATA +1 -1
- pyezvizapi-1.0.2.4.dist-info/RECORD +21 -0
- pyezvizapi-1.0.2.2.dist-info/RECORD +0 -21
- {pyezvizapi-1.0.2.2.dist-info → pyezvizapi-1.0.2.4.dist-info}/WHEEL +0 -0
- {pyezvizapi-1.0.2.2.dist-info → pyezvizapi-1.0.2.4.dist-info}/entry_points.txt +0 -0
- {pyezvizapi-1.0.2.2.dist-info → pyezvizapi-1.0.2.4.dist-info}/licenses/LICENSE +0 -0
- {pyezvizapi-1.0.2.2.dist-info → pyezvizapi-1.0.2.4.dist-info}/licenses/LICENSE.md +0 -0
- {pyezvizapi-1.0.2.2.dist-info → pyezvizapi-1.0.2.4.dist-info}/top_level.txt +0 -0
pyezvizapi/__main__.py
CHANGED
|
@@ -26,7 +26,9 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
26
26
|
def _setup_logging(debug: bool) -> None:
|
|
27
27
|
"""Configure root logger for CLI usage."""
|
|
28
28
|
level = logging.DEBUG if debug else logging.INFO
|
|
29
|
-
logging.basicConfig(
|
|
29
|
+
logging.basicConfig(
|
|
30
|
+
level=level, stream=sys.stderr, format="%(levelname)s: %(message)s"
|
|
31
|
+
)
|
|
30
32
|
if debug:
|
|
31
33
|
# Verbose requests logging in debug mode
|
|
32
34
|
requests_log = logging.getLogger("requests.packages.urllib3")
|
|
@@ -94,7 +96,7 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
|
94
96
|
type=str,
|
|
95
97
|
default="status",
|
|
96
98
|
help="Light bulbs action to perform",
|
|
97
|
-
choices=["status"]
|
|
99
|
+
choices=["status"],
|
|
98
100
|
)
|
|
99
101
|
parser_device_lights.add_argument(
|
|
100
102
|
"--refresh",
|
|
@@ -125,7 +127,9 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
|
125
127
|
|
|
126
128
|
subparsers_camera = parser_camera.add_subparsers(dest="camera_action")
|
|
127
129
|
|
|
128
|
-
parser_camera_status = subparsers_camera.add_parser(
|
|
130
|
+
parser_camera_status = subparsers_camera.add_parser(
|
|
131
|
+
"status", help="Get the status of the camera"
|
|
132
|
+
)
|
|
129
133
|
parser_camera_status.add_argument(
|
|
130
134
|
"--refresh",
|
|
131
135
|
action=argparse.BooleanOptionalAction,
|
|
@@ -229,14 +233,19 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
|
229
233
|
)
|
|
230
234
|
|
|
231
235
|
parser_camera_select = subparsers_camera.add_parser(
|
|
232
|
-
"select",
|
|
236
|
+
"select",
|
|
237
|
+
help="Change the value of a multi-value option (for on/off value, see 'switch' command)",
|
|
233
238
|
)
|
|
234
239
|
|
|
235
240
|
parser_camera_select.add_argument(
|
|
236
241
|
"--battery_work_mode",
|
|
237
242
|
required=False,
|
|
238
243
|
help="Change the work mode for battery powered camera",
|
|
239
|
-
choices=[
|
|
244
|
+
choices=[
|
|
245
|
+
mode.name
|
|
246
|
+
for mode in BatteryCameraWorkMode
|
|
247
|
+
if mode is not BatteryCameraWorkMode.UNKNOWN
|
|
248
|
+
],
|
|
240
249
|
)
|
|
241
250
|
|
|
242
251
|
# Dump full pagelist for exploration
|
|
@@ -244,7 +253,8 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
|
244
253
|
|
|
245
254
|
# Dump device infos mapping (optionally for a single serial)
|
|
246
255
|
parser_device_infos = subparsers.add_parser(
|
|
247
|
-
"device_infos",
|
|
256
|
+
"device_infos",
|
|
257
|
+
help="Output device infos (raw JSON), optionally filtered by serial",
|
|
248
258
|
)
|
|
249
259
|
parser_device_infos.add_argument(
|
|
250
260
|
"--serial", required=False, help="Optional serial to filter a single device"
|
|
@@ -418,7 +428,11 @@ def _handle_pagelist(client: EzvizClient) -> int:
|
|
|
418
428
|
|
|
419
429
|
def _handle_device_infos(args: argparse.Namespace, client: EzvizClient) -> int:
|
|
420
430
|
"""Output device infos mapping (raw JSON), optionally filtered by serial."""
|
|
421
|
-
data =
|
|
431
|
+
data = (
|
|
432
|
+
client.get_device_infos(args.serial)
|
|
433
|
+
if args.serial
|
|
434
|
+
else client.get_device_infos()
|
|
435
|
+
)
|
|
422
436
|
_write_json(data)
|
|
423
437
|
return 0
|
|
424
438
|
|
|
@@ -535,7 +549,10 @@ def _load_token_file(path: str | None) -> dict[str, Any] | None:
|
|
|
535
549
|
return None
|
|
536
550
|
try:
|
|
537
551
|
return cast(dict[str, Any], json.loads(p.read_text(encoding="utf-8")))
|
|
538
|
-
except (
|
|
552
|
+
except (
|
|
553
|
+
OSError,
|
|
554
|
+
json.JSONDecodeError,
|
|
555
|
+
): # pragma: no cover - tolerate malformed file
|
|
539
556
|
_LOGGER.warning("Failed to read token file: %s", p)
|
|
540
557
|
return None
|
|
541
558
|
|
pyezvizapi/camera.py
CHANGED
|
@@ -4,14 +4,17 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import datetime
|
|
6
6
|
import logging
|
|
7
|
-
import re
|
|
8
7
|
from typing import TYPE_CHECKING, Any, Literal, TypedDict, cast
|
|
9
|
-
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
10
8
|
|
|
11
9
|
from .constants import BatteryCameraWorkMode, DeviceSwitchType, SoundMode
|
|
12
10
|
from .exceptions import PyEzvizError
|
|
13
11
|
from .models import EzvizDeviceRecord
|
|
14
|
-
from .utils import
|
|
12
|
+
from .utils import (
|
|
13
|
+
compute_motion_from_alarm,
|
|
14
|
+
fetch_nested_value,
|
|
15
|
+
parse_timezone_value,
|
|
16
|
+
string_to_list,
|
|
17
|
+
)
|
|
15
18
|
|
|
16
19
|
if TYPE_CHECKING:
|
|
17
20
|
from .client import EzvizClient
|
|
@@ -172,110 +175,10 @@ class EzvizCamera:
|
|
|
172
175
|
|
|
173
176
|
Prefer numeric epoch fields if available to avoid parsing localized strings.
|
|
174
177
|
"""
|
|
175
|
-
# Use timezone-aware datetimes. Compute both camera-local and UTC "now".
|
|
176
178
|
tzinfo = self._get_tzinfo()
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
# Prefer epoch fields if available
|
|
181
|
-
epoch = self._last_alarm.get("alarmStartTime") or self._last_alarm.get(
|
|
182
|
-
"alarmTime"
|
|
183
|
-
)
|
|
184
|
-
last_alarm_dt: datetime.datetime | None = None
|
|
185
|
-
# Capture string time if present so we can cross-check epoch skew
|
|
186
|
-
raw_time_str = str(
|
|
187
|
-
self._last_alarm.get("alarmStartTimeStr")
|
|
188
|
-
or self._last_alarm.get("alarmTimeStr")
|
|
189
|
-
or ""
|
|
179
|
+
active, seconds_out, last_alarm_str = compute_motion_from_alarm(
|
|
180
|
+
self._last_alarm, tzinfo
|
|
190
181
|
)
|
|
191
|
-
if epoch is not None:
|
|
192
|
-
try:
|
|
193
|
-
# Accept int/float/str; auto-detect ms vs s
|
|
194
|
-
if isinstance(epoch, str):
|
|
195
|
-
epoch = float(epoch)
|
|
196
|
-
ts = float(epoch)
|
|
197
|
-
if ts > 1e11: # very likely milliseconds
|
|
198
|
-
ts = ts / 1000.0
|
|
199
|
-
# Convert epoch to UTC for robust delta; derive display time in camera tz
|
|
200
|
-
event_utc = datetime.datetime.fromtimestamp(ts, tz=datetime.UTC)
|
|
201
|
-
last_alarm_dt = event_utc.astimezone(tzinfo)
|
|
202
|
-
# Some devices appear to report epoch as local time rather than UTC.
|
|
203
|
-
# If the provided string timestamp exists and differs significantly
|
|
204
|
-
# from the epoch-based time, reinterpret the epoch as local time.
|
|
205
|
-
if raw_time_str:
|
|
206
|
-
raw_norm = raw_time_str
|
|
207
|
-
if "Today" in raw_norm:
|
|
208
|
-
raw_norm = raw_norm.replace("Today", str(now_local.date()))
|
|
209
|
-
try:
|
|
210
|
-
dt_str_local = datetime.datetime.strptime(
|
|
211
|
-
raw_norm, "%Y-%m-%d %H:%M:%S"
|
|
212
|
-
).replace(tzinfo=tzinfo)
|
|
213
|
-
diff = abs(
|
|
214
|
-
(
|
|
215
|
-
event_utc
|
|
216
|
-
- dt_str_local.astimezone(datetime.UTC)
|
|
217
|
-
).total_seconds()
|
|
218
|
-
)
|
|
219
|
-
if diff > 120:
|
|
220
|
-
# Reinterpret the epoch as local clock time in camera tz
|
|
221
|
-
naive_utc = datetime.datetime.fromtimestamp(ts, tz=datetime.UTC).replace(tzinfo=None)
|
|
222
|
-
event_local_reint = naive_utc.replace(tzinfo=tzinfo)
|
|
223
|
-
event_utc = event_local_reint.astimezone(datetime.UTC)
|
|
224
|
-
last_alarm_dt = event_local_reint
|
|
225
|
-
except ValueError:
|
|
226
|
-
pass
|
|
227
|
-
except (
|
|
228
|
-
TypeError,
|
|
229
|
-
ValueError,
|
|
230
|
-
OSError,
|
|
231
|
-
): # fall back to string parsing below
|
|
232
|
-
last_alarm_dt = None
|
|
233
|
-
|
|
234
|
-
last_alarm_str: str | None = None
|
|
235
|
-
if last_alarm_dt is None:
|
|
236
|
-
# Fall back to string parsing
|
|
237
|
-
raw = raw_time_str
|
|
238
|
-
if not raw:
|
|
239
|
-
return
|
|
240
|
-
if "Today" in raw:
|
|
241
|
-
raw = raw.replace("Today", str(now_local.date()))
|
|
242
|
-
try:
|
|
243
|
-
last_alarm_dt = datetime.datetime.strptime(
|
|
244
|
-
raw, "%Y-%m-%d %H:%M:%S"
|
|
245
|
-
).replace(tzinfo=tzinfo)
|
|
246
|
-
last_alarm_str = last_alarm_dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
247
|
-
except ValueError: # Unrecognized format; give up gracefully
|
|
248
|
-
_LOGGER.debug(
|
|
249
|
-
"Unrecognized alarm time format for %s: %s", self._serial, raw
|
|
250
|
-
)
|
|
251
|
-
self._alarmmotiontrigger = {
|
|
252
|
-
"alarm_trigger_active": False,
|
|
253
|
-
"timepassed": None,
|
|
254
|
-
"last_alarm_time_str": raw or None,
|
|
255
|
-
}
|
|
256
|
-
return
|
|
257
|
-
else:
|
|
258
|
-
# We selected epoch path; format a human-readable local string
|
|
259
|
-
last_alarm_str = last_alarm_dt.astimezone(tzinfo).strftime(
|
|
260
|
-
"%Y-%m-%d %H:%M:%S"
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
# Compute elapsed seconds since the last alarm. If the timestamp is
|
|
264
|
-
# somehow in the future (timezone mismatch or clock skew), do not
|
|
265
|
-
# report a motion trigger; clamp the exposed seconds to 0.0.
|
|
266
|
-
# Use UTC delta when epoch was provided; otherwise compute in camera tz.
|
|
267
|
-
if epoch is not None and last_alarm_dt is not None:
|
|
268
|
-
event_utc_for_delta = last_alarm_dt.astimezone(datetime.UTC)
|
|
269
|
-
delta = now_utc - event_utc_for_delta
|
|
270
|
-
else:
|
|
271
|
-
delta = now_local - last_alarm_dt
|
|
272
|
-
seconds = float(delta.total_seconds())
|
|
273
|
-
if seconds < 0:
|
|
274
|
-
active = False
|
|
275
|
-
seconds_out = 0.0
|
|
276
|
-
else:
|
|
277
|
-
active = seconds < 60.0
|
|
278
|
-
seconds_out = seconds
|
|
279
182
|
|
|
280
183
|
self._alarmmotiontrigger = {
|
|
281
184
|
"alarm_trigger_active": active,
|
|
@@ -284,41 +187,9 @@ class EzvizCamera:
|
|
|
284
187
|
}
|
|
285
188
|
|
|
286
189
|
def _get_tzinfo(self) -> datetime.tzinfo:
|
|
287
|
-
"""Return tzinfo from camera setting if recognizable, else local tzinfo.
|
|
288
|
-
|
|
289
|
-
Attempts to parse common formats like 'UTC+02:00', 'GMT+8', '+0530', or IANA names.
|
|
290
|
-
Falls back to local timezone.
|
|
291
|
-
"""
|
|
190
|
+
"""Return tzinfo from camera setting if recognizable, else local tzinfo."""
|
|
292
191
|
tz_val = self.fetch_key(["STATUS", "optionals", "timeZone"])
|
|
293
|
-
|
|
294
|
-
if isinstance(tz_val, str) and "/" in tz_val:
|
|
295
|
-
try:
|
|
296
|
-
return ZoneInfo(tz_val)
|
|
297
|
-
except ZoneInfoNotFoundError:
|
|
298
|
-
pass
|
|
299
|
-
# Offset formats
|
|
300
|
-
offset_minutes: int | None = None
|
|
301
|
-
if isinstance(tz_val, int):
|
|
302
|
-
# Heuristic: treat small absolute values as hours, large as minutes/seconds
|
|
303
|
-
if -14 <= tz_val <= 14:
|
|
304
|
-
offset_minutes = tz_val * 60
|
|
305
|
-
elif -24 * 60 <= tz_val <= 24 * 60:
|
|
306
|
-
offset_minutes = tz_val
|
|
307
|
-
elif -24 * 3600 <= tz_val <= 24 * 3600:
|
|
308
|
-
offset_minutes = int(tz_val / 60)
|
|
309
|
-
elif isinstance(tz_val, str):
|
|
310
|
-
s = tz_val.strip().upper().replace("UTC", "").replace("GMT", "")
|
|
311
|
-
# Normalize formats like '+02:00', '+0200', '+2'
|
|
312
|
-
m = re.match(r"^([+-]?)(\d{1,2})(?::?(\d{2}))?$", s)
|
|
313
|
-
if m:
|
|
314
|
-
sign = -1 if m.group(1) == "-" else 1
|
|
315
|
-
hours = int(m.group(2))
|
|
316
|
-
minutes = int(m.group(3)) if m.group(3) else 0
|
|
317
|
-
offset_minutes = sign * (hours * 60 + minutes)
|
|
318
|
-
if offset_minutes is not None:
|
|
319
|
-
return datetime.timezone(datetime.timedelta(minutes=offset_minutes))
|
|
320
|
-
# Fallback to local timezone
|
|
321
|
-
return datetime.datetime.now().astimezone().tzinfo or datetime.UTC
|
|
192
|
+
return parse_timezone_value(tz_val)
|
|
322
193
|
|
|
323
194
|
def _is_alarm_schedules_enabled(self) -> bool:
|
|
324
195
|
"""Check if alarm schedules enabled."""
|
pyezvizapi/cas.py
CHANGED
|
@@ -38,7 +38,9 @@ class EzvizCAS:
|
|
|
38
38
|
"api_url": "apiieu.ezvizlife.com",
|
|
39
39
|
}
|
|
40
40
|
if not token or "service_urls" not in token:
|
|
41
|
-
raise PyEzvizError(
|
|
41
|
+
raise PyEzvizError(
|
|
42
|
+
"Missing service_urls in token; call EzvizClient.login() first"
|
|
43
|
+
)
|
|
42
44
|
self._service_urls: dict[str, Any] = token["service_urls"]
|
|
43
45
|
|
|
44
46
|
def cas_get_encryption(self, devserial: str) -> dict[str, Any]:
|
|
@@ -53,7 +55,7 @@ class EzvizCAS:
|
|
|
53
55
|
f"\x01" # Check or order?
|
|
54
56
|
f"\x00\x00\x00\x00\x00\x00\x02\t\x00\x00\x00\x00"
|
|
55
57
|
f'<?xml version="1.0" encoding="utf-8"?>\n<Request>\n\t'
|
|
56
|
-
f
|
|
58
|
+
f"<ClientID>{self._token['session_id']}</ClientID>"
|
|
57
59
|
f"\n\t<Sign>{FEATURE_CODE}</Sign>\n\t"
|
|
58
60
|
f"<DevSerial>{devserial}</DevSerial>"
|
|
59
61
|
f"\n\t<ClientType>0</ClientType>\n</Request>\n"
|
pyezvizapi/client.py
CHANGED
|
@@ -167,7 +167,7 @@ class EzvizClient:
|
|
|
167
167
|
DeviceCatagories.BASE_STATION_DEVICE_CATEGORY.value,
|
|
168
168
|
DeviceCatagories.CAT_EYE_CATEGORY.value,
|
|
169
169
|
DeviceCatagories.LIGHTING.value,
|
|
170
|
-
DeviceCatagories.W2H_BASE_STATION_DEVICE_CATEGORY.value,
|
|
170
|
+
DeviceCatagories.W2H_BASE_STATION_DEVICE_CATEGORY.value,
|
|
171
171
|
]
|
|
172
172
|
|
|
173
173
|
def __init__(
|
|
@@ -189,7 +189,8 @@ class EzvizClient:
|
|
|
189
189
|
self._session.headers["sessionId"] = str(token["session_id"]) # ensure str
|
|
190
190
|
self._token: ClientToken = cast(
|
|
191
191
|
ClientToken,
|
|
192
|
-
token
|
|
192
|
+
token
|
|
193
|
+
or {
|
|
193
194
|
"session_id": None,
|
|
194
195
|
"rf_session_id": None,
|
|
195
196
|
"username": None,
|
|
@@ -262,7 +263,7 @@ class EzvizClient:
|
|
|
262
263
|
if json_result["meta"]["code"] == 1100:
|
|
263
264
|
self._token["api_url"] = json_result["loginArea"]["apiDomain"]
|
|
264
265
|
_LOGGER.warning(
|
|
265
|
-
"
|
|
266
|
+
"Region_incorrect: serial=%s code=%s msg=%s",
|
|
266
267
|
"unknown",
|
|
267
268
|
1100,
|
|
268
269
|
self._token["api_url"],
|
|
@@ -319,7 +320,11 @@ class EzvizClient:
|
|
|
319
320
|
)
|
|
320
321
|
req.raise_for_status()
|
|
321
322
|
except requests.HTTPError as err:
|
|
322
|
-
if
|
|
323
|
+
if (
|
|
324
|
+
retry_401
|
|
325
|
+
and err.response is not None
|
|
326
|
+
and err.response.status_code == 401
|
|
327
|
+
):
|
|
323
328
|
if max_retries >= MAX_RETRIES:
|
|
324
329
|
raise HTTPError from err
|
|
325
330
|
# Re-login and retry once
|
|
@@ -344,7 +349,10 @@ class EzvizClient:
|
|
|
344
349
|
return cast(dict, resp.json())
|
|
345
350
|
except ValueError as err:
|
|
346
351
|
raise PyEzvizError(
|
|
347
|
-
"Impossible to decode response: "
|
|
352
|
+
"Impossible to decode response: "
|
|
353
|
+
+ str(err)
|
|
354
|
+
+ "\nResponse was: "
|
|
355
|
+
+ str(resp.text)
|
|
348
356
|
) from err
|
|
349
357
|
|
|
350
358
|
@staticmethod
|
|
@@ -413,11 +421,17 @@ class EzvizClient:
|
|
|
413
421
|
req = self._session.send(request=prepared, timeout=self._timeout)
|
|
414
422
|
req.raise_for_status()
|
|
415
423
|
except requests.HTTPError as err:
|
|
416
|
-
if
|
|
424
|
+
if (
|
|
425
|
+
retry_401
|
|
426
|
+
and err.response is not None
|
|
427
|
+
and err.response.status_code == 401
|
|
428
|
+
):
|
|
417
429
|
if max_retries >= MAX_RETRIES:
|
|
418
430
|
raise HTTPError from err
|
|
419
431
|
self.login()
|
|
420
|
-
return self._send_prepared(
|
|
432
|
+
return self._send_prepared(
|
|
433
|
+
prepared, retry_401=retry_401, max_retries=max_retries + 1
|
|
434
|
+
)
|
|
421
435
|
raise HTTPError from err
|
|
422
436
|
return req
|
|
423
437
|
|
|
@@ -478,7 +492,7 @@ class EzvizClient:
|
|
|
478
492
|
# Prefer modern meta.code; fall back to legacy resultCode
|
|
479
493
|
code = self._response_code(payload)
|
|
480
494
|
_LOGGER.warning(
|
|
481
|
-
"
|
|
495
|
+
"Http_retry: serial=%s code=%s msg=%s",
|
|
482
496
|
serial or "unknown",
|
|
483
497
|
code,
|
|
484
498
|
log,
|
|
@@ -549,7 +563,7 @@ class EzvizClient:
|
|
|
549
563
|
# session is wrong, need to relogin and retry
|
|
550
564
|
self.login()
|
|
551
565
|
_LOGGER.warning(
|
|
552
|
-
"
|
|
566
|
+
"Http_retry: serial=%s code=%s msg=%s",
|
|
553
567
|
"unknown",
|
|
554
568
|
self._meta_code(json_output),
|
|
555
569
|
"pagelist_relogin",
|
|
@@ -673,7 +687,11 @@ class EzvizClient:
|
|
|
673
687
|
json_output = self._request_json(
|
|
674
688
|
"PUT",
|
|
675
689
|
f"{API_ENDPOINT_DEVICES}{serial}{API_ENDPOINT_SWITCH_OTHER}",
|
|
676
|
-
params={
|
|
690
|
+
params={
|
|
691
|
+
"channelNo": channel_number,
|
|
692
|
+
"enable": enable,
|
|
693
|
+
"switchType": status_type,
|
|
694
|
+
},
|
|
677
695
|
retry_401=True,
|
|
678
696
|
max_retries=max_retries,
|
|
679
697
|
)
|
|
@@ -704,7 +722,9 @@ class EzvizClient:
|
|
|
704
722
|
serial=serial,
|
|
705
723
|
)
|
|
706
724
|
if self._meta_code(json_output) != 200:
|
|
707
|
-
raise PyEzvizError(
|
|
725
|
+
raise PyEzvizError(
|
|
726
|
+
f"Could not arm or disarm Camera {serial}: Got {json_output})"
|
|
727
|
+
)
|
|
708
728
|
return True
|
|
709
729
|
|
|
710
730
|
def set_battery_camera_work_mode(self, serial: str, value: int) -> bool:
|
|
@@ -1017,9 +1037,14 @@ class EzvizClient:
|
|
|
1017
1037
|
self._light_bulbs[device] = EzvizLightBulb(
|
|
1018
1038
|
self, device, dict(rec.raw)
|
|
1019
1039
|
).status()
|
|
1020
|
-
except (
|
|
1040
|
+
except (
|
|
1041
|
+
PyEzvizError,
|
|
1042
|
+
KeyError,
|
|
1043
|
+
TypeError,
|
|
1044
|
+
ValueError,
|
|
1045
|
+
) as err: # pragma: no cover - defensive
|
|
1021
1046
|
_LOGGER.warning(
|
|
1022
|
-
"
|
|
1047
|
+
"Load_device_failed: serial=%s code=%s msg=%s",
|
|
1023
1048
|
device,
|
|
1024
1049
|
"load_error",
|
|
1025
1050
|
str(err),
|
|
@@ -1029,9 +1054,14 @@ class EzvizClient:
|
|
|
1029
1054
|
# Create camera object
|
|
1030
1055
|
cam = EzvizCamera(self, device, dict(rec.raw))
|
|
1031
1056
|
self._cameras[device] = cam.status(refresh=refresh)
|
|
1032
|
-
except (
|
|
1057
|
+
except (
|
|
1058
|
+
PyEzvizError,
|
|
1059
|
+
KeyError,
|
|
1060
|
+
TypeError,
|
|
1061
|
+
ValueError,
|
|
1062
|
+
) as err: # pragma: no cover - defensive
|
|
1033
1063
|
_LOGGER.warning(
|
|
1034
|
-
"
|
|
1064
|
+
"Load_device_failed: serial=%s code=%s msg=%s",
|
|
1035
1065
|
device,
|
|
1036
1066
|
"load_error",
|
|
1037
1067
|
str(err),
|
|
@@ -1100,7 +1130,9 @@ class EzvizClient:
|
|
|
1100
1130
|
try:
|
|
1101
1131
|
support_ext = result[_serial].get("deviceInfos", {}).get("supportExt")
|
|
1102
1132
|
if isinstance(support_ext, str) and support_ext:
|
|
1103
|
-
result[_serial]["deviceInfos"]["supportExt"] = json.loads(
|
|
1133
|
+
result[_serial]["deviceInfos"]["supportExt"] = json.loads(
|
|
1134
|
+
support_ext
|
|
1135
|
+
)
|
|
1104
1136
|
except (TypeError, ValueError):
|
|
1105
1137
|
# Leave as-is if not valid JSON
|
|
1106
1138
|
pass
|
|
@@ -1199,14 +1231,16 @@ class EzvizClient:
|
|
|
1199
1231
|
|
|
1200
1232
|
code = str(json_output.get("resultCode"))
|
|
1201
1233
|
if code == "20002":
|
|
1202
|
-
raise EzvizAuthVerificationCode(
|
|
1234
|
+
raise EzvizAuthVerificationCode(
|
|
1235
|
+
f"MFA code required: Got {json_output})"
|
|
1236
|
+
)
|
|
1203
1237
|
if code == "2009":
|
|
1204
1238
|
raise DeviceException(f"Device not reachable: Got {json_output})")
|
|
1205
1239
|
if code == "0":
|
|
1206
1240
|
return json_output.get("encryptkey")
|
|
1207
1241
|
if code == "-1" and attempt < attempts:
|
|
1208
1242
|
_LOGGER.warning(
|
|
1209
|
-
"
|
|
1243
|
+
"Http_retry: serial=%s code=%s msg=%s",
|
|
1210
1244
|
serial,
|
|
1211
1245
|
code,
|
|
1212
1246
|
"cam_key_not_found",
|
|
@@ -1357,7 +1391,9 @@ class EzvizClient:
|
|
|
1357
1391
|
raise PyEzvizError(
|
|
1358
1392
|
f"Could not send command to create panoramic photo: Got {json_output})"
|
|
1359
1393
|
)
|
|
1360
|
-
raise PyEzvizError(
|
|
1394
|
+
raise PyEzvizError(
|
|
1395
|
+
"Could not send command to create panoramic photo: exceeded retries"
|
|
1396
|
+
)
|
|
1361
1397
|
|
|
1362
1398
|
def return_panoramic(self, serial: str, max_retries: int = 0) -> Any:
|
|
1363
1399
|
"""Return panoramic image url list."""
|
|
@@ -1526,7 +1562,7 @@ class EzvizClient:
|
|
|
1526
1562
|
except requests.HTTPError as err:
|
|
1527
1563
|
if err.response.status_code == 401:
|
|
1528
1564
|
_LOGGER.warning(
|
|
1529
|
-
"
|
|
1565
|
+
"Http_warning: serial=%s code=%s msg=%s",
|
|
1530
1566
|
"unknown",
|
|
1531
1567
|
401,
|
|
1532
1568
|
"logout_already_invalid",
|
|
@@ -1678,7 +1714,9 @@ class EzvizClient:
|
|
|
1678
1714
|
else:
|
|
1679
1715
|
raise PyEzvizError(f"Invalid action '{action}'. Use 'add' or 'remove'.")
|
|
1680
1716
|
|
|
1681
|
-
json_output = self._request_json(
|
|
1717
|
+
json_output = self._request_json(
|
|
1718
|
+
method, url_path, retry_401=True, max_retries=max_retries
|
|
1719
|
+
)
|
|
1682
1720
|
self._ensure_ok(json_output, f"Could not {action} intelligent app")
|
|
1683
1721
|
|
|
1684
1722
|
return True
|
pyezvizapi/constants.py
CHANGED
|
@@ -464,4 +464,4 @@ class DeviceCatagories(Enum):
|
|
|
464
464
|
BASE_STATION_DEVICE_CATEGORY = "XVR"
|
|
465
465
|
CAT_EYE_CATEGORY = "CatEye"
|
|
466
466
|
LIGHTING = "lighting"
|
|
467
|
-
W2H_BASE_STATION_DEVICE_CATEGORY = "IGateWay"
|
|
467
|
+
W2H_BASE_STATION_DEVICE_CATEGORY = "IGateWay"
|
pyezvizapi/test_cam_rtsp.py
CHANGED
|
@@ -22,6 +22,7 @@ def genmsg_describe(url: str, seq: int, user_agent: str, auth_seq: str) -> str:
|
|
|
22
22
|
|
|
23
23
|
class RTSPDetails(TypedDict):
|
|
24
24
|
"""Typed structure for RTSP test parameters."""
|
|
25
|
+
|
|
25
26
|
bufLen: int
|
|
26
27
|
defaultServerIp: str
|
|
27
28
|
defaultServerPort: int
|
|
@@ -37,7 +38,11 @@ class TestRTSPAuth:
|
|
|
37
38
|
_rtsp_details: RTSPDetails
|
|
38
39
|
|
|
39
40
|
def __init__(
|
|
40
|
-
self,
|
|
41
|
+
self,
|
|
42
|
+
ip_addr: str,
|
|
43
|
+
username: str | None = None,
|
|
44
|
+
password: str | None = None,
|
|
45
|
+
test_uri: str = "",
|
|
41
46
|
) -> None:
|
|
42
47
|
"""Initialize RTSP credential test."""
|
|
43
48
|
self._rtsp_details = RTSPDetails(
|
|
@@ -50,7 +55,9 @@ class TestRTSPAuth:
|
|
|
50
55
|
defaultPassword=password,
|
|
51
56
|
)
|
|
52
57
|
|
|
53
|
-
def generate_auth_string(
|
|
58
|
+
def generate_auth_string(
|
|
59
|
+
self, realm: bytes, method: str, uri: str, nonce: bytes
|
|
60
|
+
) -> str:
|
|
54
61
|
"""Generate the HTTP Digest Authorization header value."""
|
|
55
62
|
m_1 = hashlib.md5(
|
|
56
63
|
f"{self._rtsp_details['defaultUsername']}:{realm.decode()}:{self._rtsp_details['defaultPassword']}".encode()
|
|
@@ -60,12 +67,12 @@ class TestRTSPAuth:
|
|
|
60
67
|
|
|
61
68
|
return (
|
|
62
69
|
"Digest "
|
|
63
|
-
f
|
|
64
|
-
f
|
|
70
|
+
f'username="{self._rtsp_details["defaultUsername"]}", '
|
|
71
|
+
f'realm="{realm.decode()}", '
|
|
65
72
|
'algorithm="MD5", '
|
|
66
|
-
f
|
|
67
|
-
f
|
|
68
|
-
f
|
|
73
|
+
f'nonce="{nonce.decode()}", '
|
|
74
|
+
f'uri="{uri}", '
|
|
75
|
+
f'response="{response}"'
|
|
69
76
|
)
|
|
70
77
|
|
|
71
78
|
def main(self) -> None:
|
|
@@ -74,7 +81,10 @@ class TestRTSPAuth:
|
|
|
74
81
|
|
|
75
82
|
try:
|
|
76
83
|
session.connect(
|
|
77
|
-
(
|
|
84
|
+
(
|
|
85
|
+
self._rtsp_details["defaultServerIp"],
|
|
86
|
+
self._rtsp_details["defaultServerPort"],
|
|
87
|
+
)
|
|
78
88
|
)
|
|
79
89
|
except TimeoutError as err:
|
|
80
90
|
raise AuthTestResultFailed("Invalid ip or camera hibernating") from err
|
|
@@ -83,15 +93,23 @@ class TestRTSPAuth:
|
|
|
83
93
|
|
|
84
94
|
seq: int = 1
|
|
85
95
|
|
|
86
|
-
url: str =
|
|
96
|
+
url: str = (
|
|
97
|
+
"rtsp://"
|
|
98
|
+
+ self._rtsp_details["defaultServerIp"]
|
|
99
|
+
+ self._rtsp_details["defaultTestUri"]
|
|
100
|
+
)
|
|
87
101
|
|
|
88
102
|
# Basic Authorization header
|
|
89
103
|
auth_b64: bytes = base64.b64encode(
|
|
90
|
-
f"{self._rtsp_details['defaultUsername']}:{self._rtsp_details['defaultPassword']}".encode(
|
|
104
|
+
f"{self._rtsp_details['defaultUsername']}:{self._rtsp_details['defaultPassword']}".encode(
|
|
105
|
+
"ascii"
|
|
106
|
+
)
|
|
91
107
|
)
|
|
92
108
|
auth_seq: str = "Basic " + auth_b64.decode()
|
|
93
109
|
|
|
94
|
-
describe = genmsg_describe(
|
|
110
|
+
describe = genmsg_describe(
|
|
111
|
+
url, seq, self._rtsp_details["defaultUserAgent"], auth_seq
|
|
112
|
+
)
|
|
95
113
|
print(describe)
|
|
96
114
|
session.send(describe.encode())
|
|
97
115
|
msg1: bytes = session.recv(self._rtsp_details["bufLen"])
|
|
@@ -115,9 +133,13 @@ class TestRTSPAuth:
|
|
|
115
133
|
end = decoded.find('"', begin + 1)
|
|
116
134
|
nonce: bytes = msg1[begin + 1 : end]
|
|
117
135
|
|
|
118
|
-
auth_seq = self.generate_auth_string(
|
|
136
|
+
auth_seq = self.generate_auth_string(
|
|
137
|
+
realm, "DESCRIBE", self._rtsp_details["defaultTestUri"], nonce
|
|
138
|
+
)
|
|
119
139
|
|
|
120
|
-
describe = genmsg_describe(
|
|
140
|
+
describe = genmsg_describe(
|
|
141
|
+
url, seq, self._rtsp_details["defaultUserAgent"], auth_seq
|
|
142
|
+
)
|
|
121
143
|
print(describe)
|
|
122
144
|
session.send(describe.encode())
|
|
123
145
|
msg1 = session.recv(self._rtsp_details["bufLen"])
|
|
@@ -132,4 +154,6 @@ class TestRTSPAuth:
|
|
|
132
154
|
raise AuthTestResultFailed("Credentials not valid!!")
|
|
133
155
|
|
|
134
156
|
print("Basic Auth test passed. Credentials Valid!")
|
|
157
|
+
|
|
158
|
+
|
|
135
159
|
# ruff: noqa: T201
|
pyezvizapi/utils.py
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import datetime
|
|
5
6
|
from hashlib import md5
|
|
6
7
|
import json
|
|
7
8
|
import logging
|
|
9
|
+
import re as _re
|
|
8
10
|
from typing import Any
|
|
9
11
|
import uuid
|
|
12
|
+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
10
13
|
|
|
11
14
|
from Crypto.Cipher import AES
|
|
12
15
|
|
|
@@ -188,3 +191,158 @@ def generate_unique_code() -> str:
|
|
|
188
191
|
mac_int = uuid.getnode()
|
|
189
192
|
mac_str = ":".join(f"{(mac_int >> i) & 0xFF:02x}" for i in range(40, -1, -8))
|
|
190
193
|
return md5(mac_str.encode("utf-8")).hexdigest()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
# Time helpers for alarm/motion handling
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def normalize_alarm_time(
|
|
202
|
+
last_alarm: dict[str, Any], tzinfo: datetime.tzinfo
|
|
203
|
+
) -> tuple[datetime.datetime | None, datetime.datetime | None, str | None]:
|
|
204
|
+
"""Normalize EZVIZ alarm timestamps.
|
|
205
|
+
|
|
206
|
+
Returns a tuple of:
|
|
207
|
+
- alarm_dt_local: datetime in the camera's timezone (for display)
|
|
208
|
+
- alarm_dt_utc: datetime in UTC (for robust delta calculation)
|
|
209
|
+
- alarm_time_str: formatted 'YYYY-MM-DD HH:MM:SS' string in camera tz
|
|
210
|
+
|
|
211
|
+
Behavior:
|
|
212
|
+
- Prefer epoch fields (alarmStartTime/alarmTime). Interpret as UTC by default.
|
|
213
|
+
- If a string time exists and differs from the epoch by >120 seconds,
|
|
214
|
+
reinterpret the epoch as if reported in camera local time.
|
|
215
|
+
- If no epoch, fall back to parsing the string time in the camera tz.
|
|
216
|
+
"""
|
|
217
|
+
# Prefer epoch
|
|
218
|
+
epoch = last_alarm.get("alarmStartTime") or last_alarm.get("alarmTime")
|
|
219
|
+
raw_time_str = str(
|
|
220
|
+
last_alarm.get("alarmStartTimeStr") or last_alarm.get("alarmTimeStr") or ""
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
alarm_dt_local: datetime.datetime | None = None
|
|
224
|
+
alarm_dt_utc: datetime.datetime | None = None
|
|
225
|
+
alarm_str: str | None = None
|
|
226
|
+
|
|
227
|
+
now_local = datetime.datetime.now(tz=tzinfo)
|
|
228
|
+
|
|
229
|
+
if epoch is not None:
|
|
230
|
+
try:
|
|
231
|
+
ts = float(epoch if not isinstance(epoch, str) else float(epoch))
|
|
232
|
+
if ts > 1e11: # milliseconds
|
|
233
|
+
ts /= 1000.0
|
|
234
|
+
event_utc = datetime.datetime.fromtimestamp(ts, tz=datetime.UTC)
|
|
235
|
+
alarm_dt_local = event_utc.astimezone(tzinfo)
|
|
236
|
+
alarm_dt_utc = event_utc
|
|
237
|
+
|
|
238
|
+
if raw_time_str:
|
|
239
|
+
raw_norm = raw_time_str.replace("Today", str(now_local.date()))
|
|
240
|
+
try:
|
|
241
|
+
dt_str_local = datetime.datetime.strptime(
|
|
242
|
+
raw_norm, "%Y-%m-%d %H:%M:%S"
|
|
243
|
+
).replace(tzinfo=tzinfo)
|
|
244
|
+
diff = abs(
|
|
245
|
+
(
|
|
246
|
+
event_utc - dt_str_local.astimezone(datetime.UTC)
|
|
247
|
+
).total_seconds()
|
|
248
|
+
)
|
|
249
|
+
if diff > 120:
|
|
250
|
+
# Reinterpret epoch as local clock time in camera tz
|
|
251
|
+
naive_utc = datetime.datetime.fromtimestamp(
|
|
252
|
+
ts, tz=datetime.UTC
|
|
253
|
+
).replace(tzinfo=None)
|
|
254
|
+
event_local_reint = naive_utc.replace(tzinfo=tzinfo)
|
|
255
|
+
alarm_dt_local = event_local_reint
|
|
256
|
+
alarm_dt_utc = event_local_reint.astimezone(datetime.UTC)
|
|
257
|
+
except ValueError:
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
if alarm_dt_local is not None:
|
|
261
|
+
alarm_str = alarm_dt_local.strftime("%Y-%m-%d %H:%M:%S")
|
|
262
|
+
return alarm_dt_local, alarm_dt_utc, alarm_str
|
|
263
|
+
# If conversion failed unexpectedly, fall through to string parsing
|
|
264
|
+
except (TypeError, ValueError, OSError):
|
|
265
|
+
alarm_dt_local = None
|
|
266
|
+
|
|
267
|
+
# Fallback to string parsing
|
|
268
|
+
if raw_time_str:
|
|
269
|
+
raw = raw_time_str.replace("Today", str(now_local.date()))
|
|
270
|
+
try:
|
|
271
|
+
alarm_dt_local = datetime.datetime.strptime(
|
|
272
|
+
raw, "%Y-%m-%d %H:%M:%S"
|
|
273
|
+
).replace(tzinfo=tzinfo)
|
|
274
|
+
alarm_dt_utc = alarm_dt_local.astimezone(datetime.UTC)
|
|
275
|
+
alarm_str = alarm_dt_local.strftime("%Y-%m-%d %H:%M:%S")
|
|
276
|
+
except ValueError:
|
|
277
|
+
pass
|
|
278
|
+
|
|
279
|
+
return alarm_dt_local, alarm_dt_utc, alarm_str
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def compute_motion_from_alarm(
|
|
283
|
+
last_alarm: dict[str, Any], tzinfo: datetime.tzinfo, window_seconds: float = 60.0
|
|
284
|
+
) -> tuple[bool, float, str | None]:
|
|
285
|
+
"""Compute motion state and seconds-since from an alarm payload.
|
|
286
|
+
|
|
287
|
+
Returns (active, seconds_since, last_alarm_time_str).
|
|
288
|
+
- Uses UTC for delta when epoch-derived UTC is available.
|
|
289
|
+
- Falls back to camera local tz deltas when only string times are present.
|
|
290
|
+
- Clamps negative deltas to 0.0 and deactivates motion.
|
|
291
|
+
"""
|
|
292
|
+
alarm_dt_local, alarm_dt_utc, alarm_str = normalize_alarm_time(last_alarm, tzinfo)
|
|
293
|
+
if alarm_dt_local is None:
|
|
294
|
+
return False, 0.0, None
|
|
295
|
+
|
|
296
|
+
now_local = datetime.datetime.now(tz=tzinfo).replace(microsecond=0)
|
|
297
|
+
now_utc = datetime.datetime.now(tz=datetime.UTC).replace(microsecond=0)
|
|
298
|
+
|
|
299
|
+
if alarm_dt_utc is not None:
|
|
300
|
+
delta = now_utc - alarm_dt_utc
|
|
301
|
+
else:
|
|
302
|
+
delta = now_local - alarm_dt_local
|
|
303
|
+
|
|
304
|
+
seconds = float(delta.total_seconds())
|
|
305
|
+
if seconds < 0:
|
|
306
|
+
return False, 0.0, alarm_str
|
|
307
|
+
|
|
308
|
+
return seconds < window_seconds, seconds, alarm_str
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def parse_timezone_value(tz_val: Any) -> datetime.tzinfo:
|
|
312
|
+
"""Parse EZVIZ timeZone value into a tzinfo.
|
|
313
|
+
|
|
314
|
+
Supports:
|
|
315
|
+
- IANA names like 'Europe/Paris'
|
|
316
|
+
- Offsets like 'UTC+02:00', 'GMT-5', '+0530', or integers (hours/minutes/seconds)
|
|
317
|
+
Falls back to the local system timezone, or UTC if unavailable.
|
|
318
|
+
"""
|
|
319
|
+
# IANA zone name
|
|
320
|
+
if isinstance(tz_val, str) and "/" in tz_val:
|
|
321
|
+
try:
|
|
322
|
+
return ZoneInfo(tz_val)
|
|
323
|
+
except ZoneInfoNotFoundError:
|
|
324
|
+
pass
|
|
325
|
+
|
|
326
|
+
# Numeric offsets
|
|
327
|
+
offset_minutes: int | None = None
|
|
328
|
+
if isinstance(tz_val, int):
|
|
329
|
+
if -14 <= tz_val <= 14:
|
|
330
|
+
offset_minutes = tz_val * 60
|
|
331
|
+
elif -24 * 60 <= tz_val <= 24 * 60:
|
|
332
|
+
offset_minutes = tz_val
|
|
333
|
+
elif -24 * 3600 <= tz_val <= 24 * 3600:
|
|
334
|
+
offset_minutes = int(tz_val / 60)
|
|
335
|
+
elif isinstance(tz_val, str):
|
|
336
|
+
s = tz_val.strip().upper().replace("UTC", "").replace("GMT", "")
|
|
337
|
+
m = _re.match(r"^([+-]?)(\d{1,2})(?::?(\d{2}))?$", s)
|
|
338
|
+
if m:
|
|
339
|
+
sign = -1 if m.group(1) == "-" else 1
|
|
340
|
+
hours = int(m.group(2))
|
|
341
|
+
minutes = int(m.group(3)) if m.group(3) else 0
|
|
342
|
+
offset_minutes = sign * (hours * 60 + minutes)
|
|
343
|
+
|
|
344
|
+
if offset_minutes is not None:
|
|
345
|
+
return datetime.timezone(datetime.timedelta(minutes=offset_minutes))
|
|
346
|
+
|
|
347
|
+
# Fallbacks
|
|
348
|
+
return datetime.datetime.now().astimezone().tzinfo or datetime.UTC
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
pyezvizapi/__init__.py,sha256=IDnIN_nfIISVwuy0cVBh4wspgAav6MuOJCQGajjyU3g,1881
|
|
2
|
+
pyezvizapi/__main__.py,sha256=9uttTuOfO22tzyomJIV8ebFJ-G-YUNDYOadZ_0AgdNA,20925
|
|
3
|
+
pyezvizapi/api_endpoints.py,sha256=rk6VinLVCn-B6DxnhfV79liplNpgUsipNbTEa_MRVwU,2755
|
|
4
|
+
pyezvizapi/camera.py,sha256=Pl5oIEdrFcv1Hz5sQI1IyyJIDCMjOjQdtExgKzmLoK8,22102
|
|
5
|
+
pyezvizapi/cas.py,sha256=3zHe-_a0KchCmGeAj1of-pV6oMPRUmSCIiDqBFsTK8A,6025
|
|
6
|
+
pyezvizapi/client.py,sha256=I20oMXw8sVVmuvgVj7eC1J6KFK_ziLM0OsJRpMuR6xs,72418
|
|
7
|
+
pyezvizapi/constants.py,sha256=RPyKUj0OX2VypIzcGcmmnLPDvDlCMUaZGpwADB53q4U,12585
|
|
8
|
+
pyezvizapi/exceptions.py,sha256=8rmxEUQdrziqMe-M1SeeRd0HtP2IDQ2xpJVj7wvOQyo,976
|
|
9
|
+
pyezvizapi/light_bulb.py,sha256=9wgycG3dTvBbrsxQjQnXal-GA8VXPsIN1m-CTtRh8i0,7797
|
|
10
|
+
pyezvizapi/models.py,sha256=NQzwTP0yEe2IWU-Vc6nAn87xulpTuo0MX2Rcf0WxifA,4176
|
|
11
|
+
pyezvizapi/mqtt.py,sha256=aOL-gexZgYvCCaNQ03M4vZan91d5p2Fl_qsFykn9NW4,22365
|
|
12
|
+
pyezvizapi/test_cam_rtsp.py,sha256=O9NHh-vcNFfnzNw8jbuhM9a_5TWfNZIMXaJP7Lmkaj4,5162
|
|
13
|
+
pyezvizapi/test_mqtt.py,sha256=Orn-fwZPJIE4G5KROMX0MRAkLwU6nLb9LUtXyb2ZCQs,4147
|
|
14
|
+
pyezvizapi/utils.py,sha256=G8gGjG0ecdN05Y0vxOHvcQMtQXgVB7nHzyvCzz66kLk,12148
|
|
15
|
+
pyezvizapi-1.0.2.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
16
|
+
pyezvizapi-1.0.2.4.dist-info/licenses/LICENSE.md,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
17
|
+
pyezvizapi-1.0.2.4.dist-info/METADATA,sha256=DnOoi3MRCjee1zRpnunI115JXlwTp3Of9Z2fBN_f1Z4,695
|
|
18
|
+
pyezvizapi-1.0.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
19
|
+
pyezvizapi-1.0.2.4.dist-info/entry_points.txt,sha256=_BSJ3eNb2H_AZkRdsv1s4mojqWn3N7m503ujvg1SudA,56
|
|
20
|
+
pyezvizapi-1.0.2.4.dist-info/top_level.txt,sha256=gMZTelIi8z7pXyTCQLLaIkxVRrDQ_lS2NEv0WgfHrHs,11
|
|
21
|
+
pyezvizapi-1.0.2.4.dist-info/RECORD,,
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
pyezvizapi/__init__.py,sha256=IDnIN_nfIISVwuy0cVBh4wspgAav6MuOJCQGajjyU3g,1881
|
|
2
|
-
pyezvizapi/__main__.py,sha256=SeV954H-AV-U1thNxRd7rWTGtSlfWyNzdrjF8gikYus,20777
|
|
3
|
-
pyezvizapi/api_endpoints.py,sha256=rk6VinLVCn-B6DxnhfV79liplNpgUsipNbTEa_MRVwU,2755
|
|
4
|
-
pyezvizapi/camera.py,sha256=8F8oyLnvZgl67Id6_R1m3OuhnDPNV92mTKGyLAKIFy8,28250
|
|
5
|
-
pyezvizapi/cas.py,sha256=ISmb-eTPuacI10L87lULbQ-oDMBuygO7Tf-s9f-tvYE,5995
|
|
6
|
-
pyezvizapi/client.py,sha256=Bp5eQbn4-pjsZicfpWy6jD5bDjQeYFw-SN1p0uzKJRY,71782
|
|
7
|
-
pyezvizapi/constants.py,sha256=SqdJRQSRdVYQxMgJa__AgorzdWglgA4MM4H2fq3QLAE,12633
|
|
8
|
-
pyezvizapi/exceptions.py,sha256=8rmxEUQdrziqMe-M1SeeRd0HtP2IDQ2xpJVj7wvOQyo,976
|
|
9
|
-
pyezvizapi/light_bulb.py,sha256=9wgycG3dTvBbrsxQjQnXal-GA8VXPsIN1m-CTtRh8i0,7797
|
|
10
|
-
pyezvizapi/models.py,sha256=NQzwTP0yEe2IWU-Vc6nAn87xulpTuo0MX2Rcf0WxifA,4176
|
|
11
|
-
pyezvizapi/mqtt.py,sha256=aOL-gexZgYvCCaNQ03M4vZan91d5p2Fl_qsFykn9NW4,22365
|
|
12
|
-
pyezvizapi/test_cam_rtsp.py,sha256=WGSM5EiOTl_r1mWHoMb7bXHm_BCn1P9X_669YQ38r6k,4903
|
|
13
|
-
pyezvizapi/test_mqtt.py,sha256=Orn-fwZPJIE4G5KROMX0MRAkLwU6nLb9LUtXyb2ZCQs,4147
|
|
14
|
-
pyezvizapi/utils.py,sha256=o342o3LI9eP8qla1vXM2rqlVbdTiLK0dAqrkyUSXpg8,5939
|
|
15
|
-
pyezvizapi-1.0.2.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
16
|
-
pyezvizapi-1.0.2.2.dist-info/licenses/LICENSE.md,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
17
|
-
pyezvizapi-1.0.2.2.dist-info/METADATA,sha256=aY8Dh0Ia8j4gTVBxgJW_JnoGFDumEfAfQTDMvNqZI2U,695
|
|
18
|
-
pyezvizapi-1.0.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
19
|
-
pyezvizapi-1.0.2.2.dist-info/entry_points.txt,sha256=_BSJ3eNb2H_AZkRdsv1s4mojqWn3N7m503ujvg1SudA,56
|
|
20
|
-
pyezvizapi-1.0.2.2.dist-info/top_level.txt,sha256=gMZTelIi8z7pXyTCQLLaIkxVRrDQ_lS2NEv0WgfHrHs,11
|
|
21
|
-
pyezvizapi-1.0.2.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|