pyezvizapi 1.0.2.1__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",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyezvizapi
3
- Version: 1.0.2.1
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,7 +1,7 @@
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
6
  pyezvizapi/client.py,sha256=Bp5eQbn4-pjsZicfpWy6jD5bDjQeYFw-SN1p0uzKJRY,71782
7
7
  pyezvizapi/constants.py,sha256=SqdJRQSRdVYQxMgJa__AgorzdWglgA4MM4H2fq3QLAE,12633
@@ -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.2.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
16
- pyezvizapi-1.0.2.1.dist-info/licenses/LICENSE.md,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
17
- pyezvizapi-1.0.2.1.dist-info/METADATA,sha256=7bfpp78cETKjTT4kfylOsfEqxiLFRBJ2w8X8tcalz5Y,695
18
- pyezvizapi-1.0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
- pyezvizapi-1.0.2.1.dist-info/entry_points.txt,sha256=_BSJ3eNb2H_AZkRdsv1s4mojqWn3N7m503ujvg1SudA,56
20
- pyezvizapi-1.0.2.1.dist-info/top_level.txt,sha256=gMZTelIi8z7pXyTCQLLaIkxVRrDQ_lS2NEv0WgfHrHs,11
21
- pyezvizapi-1.0.2.1.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,,