From 317d1acfe105fc01f4ee893b73c0ae3b7d1a0b83 Mon Sep 17 00:00:00 2001 From: Eric W Date: Sun, 29 Mar 2026 08:43:42 -0400 Subject: [PATCH] Fix motion activity endpoint returning invalid timestamps after pandas 3.0 upgrade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pandas 3.0 changed DatetimeIndex internal storage from datetime64[ns] (nanoseconds) to datetime64[us] (microseconds). The motion activity endpoint in review.py converted DatetimeIndex to epoch seconds using: df.index = df.index.astype(int) // (10**9) This assumed nanosecond resolution, dividing by 10^9 to get seconds. With microsecond resolution the division produces values ~1000x too small (e.g. 1774785 instead of 1774785600), causing every entry to have a start_time near zero. The frontend timeline could not match these timestamps to the visible range, so motion indicator bars disappeared entirely — despite the underlying recording data being correct. Replace the resolution-dependent integer division with pandas Timedelta arithmetic: df.index = (df.index - _EPOCH) // _ONE_SECOND This is resolution-independent (produces correct results on datetime64[s], [ms], [us], and [ns]), ~148x faster than the per-element .timestamp() alternative, produces native Python int types that serialize cleanly to JSON, and is backwards-compatible with older pandas versions. Co-Authored-By: Claude Opus 4.6 --- frigate/api/review.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frigate/api/review.py b/frigate/api/review.py index d2e8063d5..e5989307b 100644 --- a/frigate/api/review.py +++ b/frigate/api/review.py @@ -40,6 +40,11 @@ from frigate.util.time import get_dst_transitions logger = logging.getLogger(__name__) +# Pre-computed constants for resolution-independent datetime-to-epoch conversion +# (pandas 3.0+ stores datetime64 as microseconds, not nanoseconds) +_EPOCH = pd.Timestamp("1970-01-01") +_ONE_SECOND = pd.Timedelta("1s") + router = APIRouter(tags=[Tags.review]) @@ -659,7 +664,7 @@ def motion_activity( df.iloc[i : i + chunk, 0] = 0.0 # change types for output - df.index = df.index.astype(int) // (10**9) + df.index = (df.index - _EPOCH) // _ONE_SECOND normalized = df.reset_index().to_dict("records") return JSONResponse(content=normalized)