pyezvizapi 1.0.1.9__tar.gz → 1.0.2.2__tar.gz
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-1.0.1.9/pyezvizapi.egg-info → pyezvizapi-1.0.2.2}/PKG-INFO +1 -1
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/pyezvizapi/camera.py +71 -14
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/pyezvizapi/client.py +17 -11
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/pyezvizapi/constants.py +1 -0
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2/pyezvizapi.egg-info}/PKG-INFO +1 -1
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/setup.py +1 -1
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/LICENSE +0 -0
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/LICENSE.md +0 -0
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/MANIFEST.in +0 -0
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/README.md +0 -0
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/pyezvizapi/__init__.py +0 -0
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/pyezvizapi/__main__.py +0 -0
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/pyezvizapi/api_endpoints.py +0 -0
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/pyezvizapi/cas.py +0 -0
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/pyezvizapi/exceptions.py +0 -0
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/pyezvizapi/light_bulb.py +0 -0
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/pyezvizapi/models.py +0 -0
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/pyezvizapi/mqtt.py +0 -0
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/pyezvizapi/test_cam_rtsp.py +0 -0
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/pyezvizapi/test_mqtt.py +0 -0
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/pyezvizapi/utils.py +0 -0
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/pyezvizapi.egg-info/SOURCES.txt +0 -0
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/pyezvizapi.egg-info/dependency_links.txt +0 -0
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/pyezvizapi.egg-info/entry_points.txt +0 -0
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/pyezvizapi.egg-info/requires.txt +0 -0
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/pyezvizapi.egg-info/top_level.txt +0 -0
- {pyezvizapi-1.0.1.9 → pyezvizapi-1.0.2.2}/setup.cfg +0 -0
|
@@ -99,6 +99,7 @@ class EzvizCamera:
|
|
|
99
99
|
self._alarmmotiontrigger: dict[str, Any] = {
|
|
100
100
|
"alarm_trigger_active": False,
|
|
101
101
|
"timepassed": None,
|
|
102
|
+
"last_alarm_time_str": None,
|
|
102
103
|
}
|
|
103
104
|
self._record: EzvizDeviceRecord | None = None
|
|
104
105
|
|
|
@@ -171,15 +172,22 @@ class EzvizCamera:
|
|
|
171
172
|
|
|
172
173
|
Prefer numeric epoch fields if available to avoid parsing localized strings.
|
|
173
174
|
"""
|
|
174
|
-
# Use timezone-aware datetimes
|
|
175
|
+
# Use timezone-aware datetimes. Compute both camera-local and UTC "now".
|
|
175
176
|
tzinfo = self._get_tzinfo()
|
|
176
|
-
|
|
177
|
+
now_local = datetime.datetime.now(tz=tzinfo).replace(microsecond=0)
|
|
178
|
+
now_utc = datetime.datetime.now(tz=datetime.UTC).replace(microsecond=0)
|
|
177
179
|
|
|
178
180
|
# Prefer epoch fields if available
|
|
179
181
|
epoch = self._last_alarm.get("alarmStartTime") or self._last_alarm.get(
|
|
180
182
|
"alarmTime"
|
|
181
183
|
)
|
|
182
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 ""
|
|
190
|
+
)
|
|
183
191
|
if epoch is not None:
|
|
184
192
|
try:
|
|
185
193
|
# Accept int/float/str; auto-detect ms vs s
|
|
@@ -188,7 +196,34 @@ class EzvizCamera:
|
|
|
188
196
|
ts = float(epoch)
|
|
189
197
|
if ts > 1e11: # very likely milliseconds
|
|
190
198
|
ts = ts / 1000.0
|
|
191
|
-
|
|
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
|
|
192
227
|
except (
|
|
193
228
|
TypeError,
|
|
194
229
|
ValueError,
|
|
@@ -196,21 +231,19 @@ class EzvizCamera:
|
|
|
196
231
|
): # fall back to string parsing below
|
|
197
232
|
last_alarm_dt = None
|
|
198
233
|
|
|
234
|
+
last_alarm_str: str | None = None
|
|
199
235
|
if last_alarm_dt is None:
|
|
200
236
|
# Fall back to string parsing
|
|
201
|
-
raw =
|
|
202
|
-
self._last_alarm.get("alarmStartTimeStr")
|
|
203
|
-
or self._last_alarm.get("alarmTimeStr")
|
|
204
|
-
or ""
|
|
205
|
-
)
|
|
237
|
+
raw = raw_time_str
|
|
206
238
|
if not raw:
|
|
207
239
|
return
|
|
208
240
|
if "Today" in raw:
|
|
209
|
-
raw = raw.replace("Today", str(
|
|
241
|
+
raw = raw.replace("Today", str(now_local.date()))
|
|
210
242
|
try:
|
|
211
243
|
last_alarm_dt = datetime.datetime.strptime(
|
|
212
244
|
raw, "%Y-%m-%d %H:%M:%S"
|
|
213
245
|
).replace(tzinfo=tzinfo)
|
|
246
|
+
last_alarm_str = last_alarm_dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
214
247
|
except ValueError: # Unrecognized format; give up gracefully
|
|
215
248
|
_LOGGER.debug(
|
|
216
249
|
"Unrecognized alarm time format for %s: %s", self._serial, raw
|
|
@@ -218,15 +251,36 @@ class EzvizCamera:
|
|
|
218
251
|
self._alarmmotiontrigger = {
|
|
219
252
|
"alarm_trigger_active": False,
|
|
220
253
|
"timepassed": None,
|
|
254
|
+
"last_alarm_time_str": raw or None,
|
|
221
255
|
}
|
|
222
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
|
+
)
|
|
223
262
|
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
226
279
|
|
|
227
280
|
self._alarmmotiontrigger = {
|
|
228
|
-
"alarm_trigger_active":
|
|
229
|
-
"timepassed":
|
|
281
|
+
"alarm_trigger_active": active,
|
|
282
|
+
"timepassed": seconds_out,
|
|
283
|
+
"last_alarm_time_str": last_alarm_str,
|
|
230
284
|
}
|
|
231
285
|
|
|
232
286
|
def _get_tzinfo(self) -> datetime.tzinfo:
|
|
@@ -370,7 +424,10 @@ class EzvizCamera:
|
|
|
370
424
|
"PIR_Status": self.fetch_key(["STATUS", "pirStatus"]),
|
|
371
425
|
"Motion_Trigger": self._alarmmotiontrigger["alarm_trigger_active"],
|
|
372
426
|
"Seconds_Last_Trigger": self._alarmmotiontrigger["timepassed"],
|
|
373
|
-
|
|
427
|
+
# Keep last_alarm_time in sync with the time actually used to
|
|
428
|
+
# compute Motion_Trigger/Seconds_Last_Trigger.
|
|
429
|
+
"last_alarm_time": self._alarmmotiontrigger.get("last_alarm_time_str")
|
|
430
|
+
or self._last_alarm.get("alarmStartTimeStr"),
|
|
374
431
|
"last_alarm_pic": self._last_alarm.get(
|
|
375
432
|
"picUrl",
|
|
376
433
|
"https://eustatics.ezvizlife.com/ovs_mall/web/img/index/EZVIZ_logo.png?ver=3007907502",
|
|
@@ -167,6 +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, # @emeric699 Adding support for W2H Base Station
|
|
170
171
|
]
|
|
171
172
|
|
|
172
173
|
def __init__(
|
|
@@ -557,7 +558,8 @@ class EzvizClient:
|
|
|
557
558
|
page_filter, json_key, group_id, limit, offset, max_retries + 1
|
|
558
559
|
)
|
|
559
560
|
|
|
560
|
-
|
|
561
|
+
page_info = json_output.get("page") or {}
|
|
562
|
+
next_page = bool(page_info.get("hasNext", False))
|
|
561
563
|
|
|
562
564
|
data = json_output[json_key] if json_key else json_output
|
|
563
565
|
|
|
@@ -990,10 +992,11 @@ class EzvizClient:
|
|
|
990
992
|
|
|
991
993
|
refresh: if True, camera.status() may perform network fetches (e.g. alarms).
|
|
992
994
|
Returns a combined mapping of serial -> status dict for both cameras and bulbs.
|
|
995
|
+
|
|
996
|
+
Note: We update in place and do not remove keys for devices that may
|
|
997
|
+
have disappeared. Users who intentionally remove a device can restart
|
|
998
|
+
the integration to flush stale entries.
|
|
993
999
|
"""
|
|
994
|
-
# Reset caches to reflect the current device roster
|
|
995
|
-
self._cameras.clear()
|
|
996
|
-
self._light_bulbs.clear()
|
|
997
1000
|
|
|
998
1001
|
# Build lightweight records for clean gating/selection
|
|
999
1002
|
records = cast(dict[str, EzvizDeviceRecord], self.get_device_records(None))
|
|
@@ -1033,7 +1036,6 @@ class EzvizClient:
|
|
|
1033
1036
|
"load_error",
|
|
1034
1037
|
str(err),
|
|
1035
1038
|
)
|
|
1036
|
-
|
|
1037
1039
|
return {**self._cameras, **self._light_bulbs}
|
|
1038
1040
|
|
|
1039
1041
|
def load_cameras(self, refresh: bool = True) -> dict[Any, Any]:
|
|
@@ -1058,7 +1060,7 @@ class EzvizClient:
|
|
|
1058
1060
|
result: dict[str, Any] = {}
|
|
1059
1061
|
_res_id = "NONE"
|
|
1060
1062
|
|
|
1061
|
-
for device in devices
|
|
1063
|
+
for device in devices.get("deviceInfos", []) or []:
|
|
1062
1064
|
_serial = device["deviceSerial"]
|
|
1063
1065
|
_res_id_list = {
|
|
1064
1066
|
item
|
|
@@ -1088,16 +1090,20 @@ class EzvizClient:
|
|
|
1088
1090
|
},
|
|
1089
1091
|
"resourceInfos": [
|
|
1090
1092
|
item
|
|
1091
|
-
for item in devices.get("resourceInfos")
|
|
1092
|
-
if item.get("deviceSerial") == _serial
|
|
1093
|
+
for item in (devices.get("resourceInfos") or [])
|
|
1094
|
+
if isinstance(item, dict) and item.get("deviceSerial") == _serial
|
|
1093
1095
|
], # Could be more than one
|
|
1094
1096
|
"WIFI": devices.get("WIFI", {}).get(_serial, {}),
|
|
1095
1097
|
"deviceInfos": device,
|
|
1096
1098
|
}
|
|
1097
1099
|
# Nested keys are still encoded as JSON strings
|
|
1098
|
-
|
|
1099
|
-
result[_serial]
|
|
1100
|
-
|
|
1100
|
+
try:
|
|
1101
|
+
support_ext = result[_serial].get("deviceInfos", {}).get("supportExt")
|
|
1102
|
+
if isinstance(support_ext, str) and support_ext:
|
|
1103
|
+
result[_serial]["deviceInfos"]["supportExt"] = json.loads(support_ext)
|
|
1104
|
+
except (TypeError, ValueError):
|
|
1105
|
+
# Leave as-is if not valid JSON
|
|
1106
|
+
pass
|
|
1101
1107
|
convert_to_dict(result[_serial]["STATUS"].get("optionals"))
|
|
1102
1108
|
|
|
1103
1109
|
if not serial:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|