pyezvizapi 1.0.1.9__py3-none-any.whl → 1.0.2.2__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/camera.py CHANGED
@@ -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 based on camera or local timezone.
175
+ # Use timezone-aware datetimes. Compute both camera-local and UTC "now".
175
176
  tzinfo = self._get_tzinfo()
176
- now = datetime.datetime.now(tz=tzinfo).replace(microsecond=0)
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
- last_alarm_dt = datetime.datetime.fromtimestamp(ts, tz=tzinfo)
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 = str(
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(now.date()))
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
- timepassed = now - last_alarm_dt
225
- seconds = max(0.0, timepassed.total_seconds()) if timepassed else None
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": bool(timepassed < datetime.timedelta(seconds=60)),
229
- "timepassed": seconds,
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
- "last_alarm_time": self._last_alarm.get("alarmStartTimeStr"),
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",
pyezvizapi/client.py CHANGED
@@ -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
- next_page = json_output["page"].get("hasNext", False)
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["deviceInfos"]:
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
- result[_serial]["deviceInfos"]["supportExt"] = json.loads(
1099
- result[_serial]["deviceInfos"]["supportExt"]
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:
pyezvizapi/constants.py CHANGED
@@ -464,3 +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" # @emeric699 Adding support for W2H Base Station
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyezvizapi
3
- Version: 1.0.1.9
3
+ Version: 1.0.2.2
4
4
  Summary: Pilot your Ezviz cameras
5
5
  Home-page: https://github.com/RenierM26/pyEzvizApi/
6
6
  Author: Renier Moorcroft
@@ -1,10 +1,10 @@
1
1
  pyezvizapi/__init__.py,sha256=IDnIN_nfIISVwuy0cVBh4wspgAav6MuOJCQGajjyU3g,1881
2
2
  pyezvizapi/__main__.py,sha256=SeV954H-AV-U1thNxRd7rWTGtSlfWyNzdrjF8gikYus,20777
3
3
  pyezvizapi/api_endpoints.py,sha256=rk6VinLVCn-B6DxnhfV79liplNpgUsipNbTEa_MRVwU,2755
4
- pyezvizapi/camera.py,sha256=Vpuh7RkUBfSmNCFAXaALzfvvL0RD3SzJJyWqwZzWuHk,25191
4
+ pyezvizapi/camera.py,sha256=8F8oyLnvZgl67Id6_R1m3OuhnDPNV92mTKGyLAKIFy8,28250
5
5
  pyezvizapi/cas.py,sha256=ISmb-eTPuacI10L87lULbQ-oDMBuygO7Tf-s9f-tvYE,5995
6
- pyezvizapi/client.py,sha256=dcHAsdtfSTkVCrkaEZKpFggDXX4t5debpuxc76sOy-k,71267
7
- pyezvizapi/constants.py,sha256=R0zGg8Rv59An36dSXQLl3WU7VbpTbpo2comk9VJc68k,12535
6
+ pyezvizapi/client.py,sha256=Bp5eQbn4-pjsZicfpWy6jD5bDjQeYFw-SN1p0uzKJRY,71782
7
+ pyezvizapi/constants.py,sha256=SqdJRQSRdVYQxMgJa__AgorzdWglgA4MM4H2fq3QLAE,12633
8
8
  pyezvizapi/exceptions.py,sha256=8rmxEUQdrziqMe-M1SeeRd0HtP2IDQ2xpJVj7wvOQyo,976
9
9
  pyezvizapi/light_bulb.py,sha256=9wgycG3dTvBbrsxQjQnXal-GA8VXPsIN1m-CTtRh8i0,7797
10
10
  pyezvizapi/models.py,sha256=NQzwTP0yEe2IWU-Vc6nAn87xulpTuo0MX2Rcf0WxifA,4176
@@ -12,10 +12,10 @@ pyezvizapi/mqtt.py,sha256=aOL-gexZgYvCCaNQ03M4vZan91d5p2Fl_qsFykn9NW4,22365
12
12
  pyezvizapi/test_cam_rtsp.py,sha256=WGSM5EiOTl_r1mWHoMb7bXHm_BCn1P9X_669YQ38r6k,4903
13
13
  pyezvizapi/test_mqtt.py,sha256=Orn-fwZPJIE4G5KROMX0MRAkLwU6nLb9LUtXyb2ZCQs,4147
14
14
  pyezvizapi/utils.py,sha256=o342o3LI9eP8qla1vXM2rqlVbdTiLK0dAqrkyUSXpg8,5939
15
- pyezvizapi-1.0.1.9.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
16
- pyezvizapi-1.0.1.9.dist-info/licenses/LICENSE.md,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
17
- pyezvizapi-1.0.1.9.dist-info/METADATA,sha256=kZWGIVtf7BcQ1WJRCFCDQZmh1NibUNQs_S6TONkU-CQ,695
18
- pyezvizapi-1.0.1.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
- pyezvizapi-1.0.1.9.dist-info/entry_points.txt,sha256=_BSJ3eNb2H_AZkRdsv1s4mojqWn3N7m503ujvg1SudA,56
20
- pyezvizapi-1.0.1.9.dist-info/top_level.txt,sha256=gMZTelIi8z7pXyTCQLLaIkxVRrDQ_lS2NEv0WgfHrHs,11
21
- pyezvizapi-1.0.1.9.dist-info/RECORD,,
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,,