diff --git a/docker/main/requirements-wheels.txt b/docker/main/requirements-wheels.txt index f4167744e..5c98571be 100644 --- a/docker/main/requirements-wheels.txt +++ b/docker/main/requirements-wheels.txt @@ -7,6 +7,7 @@ numpy == 1.23.* onvif_zeep == 0.2.12 opencv-python-headless == 4.7.0.* paho-mqtt == 1.6.* +pandas == 2.1.4 peewee == 3.17.* peewee_migrate == 1.12.* psutil == 5.9.* diff --git a/frigate/http.py b/frigate/http.py index 4c7a01b67..1d9875405 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -16,6 +16,7 @@ from urllib.parse import unquote import cv2 import numpy as np +import pandas as pd import pytz import requests from flask import ( @@ -739,41 +740,59 @@ def hourly_timeline_activity(camera_name: str): # set initial start so data is representative of full hour hours[int(key.timestamp())].append( - { - "date": key.timestamp(), - "count": 0, - "type": "motion", - } + [ + key.timestamp(), + 0, + False, + ] ) for recording in all_recordings: if recording.start_time > check: hours[int(key.timestamp())].append( - { - "date": (key + timedelta(hours=1)).timestamp(), - "count": 0, - "type": "motion", - } + [ + (key + timedelta(minutes=59, seconds=59)).timestamp(), + 0, + False, + ] ) key = key + timedelta(hours=1) check = (key + timedelta(hours=1)).timestamp() hours[int(key.timestamp())].append( - { - "date": key.timestamp(), - "count": 0, - "type": "motion", - } + [ + key.timestamp(), + 0, + False, + ] ) - data_type = "motion" if recording.objects == 0 else "objects" + data_type = recording.objects > 0 hours[int(key.timestamp())].append( - { - "date": recording.start_time + (recording.duration / 2), - "count": recording.motion, - "type": data_type, - } + [ + recording.start_time + (recording.duration / 2), + recording.motion, + data_type, + ] ) + # resample data using pandas to get activity on minute to minute basis + for key, data in hours.items(): + df = pd.DataFrame(data, columns=["date", "count", "hasObjects"]) + + # set date as datetime index + df["date"] = pd.to_datetime(df["date"], unit="s") + df.set_index(["date"], inplace=True) + + # normalize data + df["count"] = np.log10(df["count"], where=df["count"] > 0) + df = df.resample("T").mean().fillna(0) + + # change types for output + df.index = (df.index.astype(int) // (10 ** 9)) + df["count"] = df["count"].astype(int) + df["hasObjects"] = df["hasObjects"].astype(bool) + hours[key] = df.reset_index().to_dict('records') + return jsonify(hours) diff --git a/web/src/components/graph/TimelineGraph.tsx b/web/src/components/graph/TimelineGraph.tsx index ddffe59ac..3cc7e22e4 100644 --- a/web/src/components/graph/TimelineGraph.tsx +++ b/web/src/components/graph/TimelineGraph.tsx @@ -4,17 +4,34 @@ import Chart from "react-apexcharts"; type TimelineGraphProps = { id: string; data: GraphData[]; + start: number; + end: number; + objects: number[]; }; /** * A graph meant to be overlaid on top of a timeline */ -export default function TimelineGraph({ id, data }: TimelineGraphProps) { +export default function TimelineGraph({ + id, + data, + start, + end, + objects, +}: TimelineGraphProps) { return ( { + if (objects.includes(dataPointIndex)) { + return "#06b6d4"; + } else { + return "#991b1b"; + } + }, + ], chart: { id: id, selection: { @@ -30,11 +47,27 @@ export default function TimelineGraph({ id, data }: TimelineGraphProps) { dataLabels: { enabled: false }, grid: { show: false, + padding: { + bottom: 20, + top: -12, + left: -20, + right: 0, + }, }, legend: { show: false, position: "top", }, + plotOptions: { + bar: { + columnWidth: "100%", + barHeight: "100%", + hideZeroBarsWhenGrouped: true, + }, + }, + stroke: { + width: 0, + }, tooltip: { enabled: false, }, @@ -49,13 +82,16 @@ export default function TimelineGraph({ id, data }: TimelineGraphProps) { labels: { show: false, }, + min: start, + max: end, }, yaxis: { + axisBorder: { + show: false, + }, labels: { show: false, }, - logarithmic: true, - logBase: 10, }, }} series={data} diff --git a/web/src/pages/ConfigEditor.tsx b/web/src/pages/ConfigEditor.tsx index 332e31855..629570d91 100644 --- a/web/src/pages/ConfigEditor.tsx +++ b/web/src/pages/ConfigEditor.tsx @@ -122,7 +122,7 @@ function ConfigEditor() { } return ( -
+
Config
diff --git a/web/src/types/record.ts b/web/src/types/record.ts index 614d223a8..7be7edb8a 100644 --- a/web/src/types/record.ts +++ b/web/src/types/record.ts @@ -26,5 +26,5 @@ type RecordingActivity = { type RecordingSegmentActivity = { date: number; count: number; - type: "motion" | "objects"; + hasObjects: boolean; }; diff --git a/web/src/utils/historyUtil.ts b/web/src/utils/historyUtil.ts index d9c6b88c0..94eaf2941 100644 --- a/web/src/utils/historyUtil.ts +++ b/web/src/utils/historyUtil.ts @@ -149,10 +149,8 @@ export function getTimelineHoursForDay( end = startDay.getTime() / 1000; const hour = Object.values(day).find((cards) => { - if ( - Object.values(cards)[0].time < start || - Object.values(cards)[0].time > end - ) { + const card = Object.values(cards)[0]; + if (card == undefined || card.time < start || card.time > end) { return false; } diff --git a/web/src/views/history/DesktopTimelineView.tsx b/web/src/views/history/DesktopTimelineView.tsx index bbdd9625d..1aadefbda 100644 --- a/web/src/views/history/DesktopTimelineView.tsx +++ b/web/src/views/history/DesktopTimelineView.tsx @@ -185,25 +185,22 @@ export default function DesktopTimelineView({ } const graphData: { - [hour: string]: { objects: GraphDataPoint[]; motion: GraphDataPoint[] }; + [hour: string]: { objects: number[]; motion: GraphDataPoint[] }; } = {}; Object.entries(activity).forEach(([hour, data]) => { - const objects: GraphDataPoint[] = []; + const objects: number[] = []; const motion: GraphDataPoint[] = []; - data.forEach((seg) => { - if (seg.type == "objects") { - objects.push({ - x: new Date(seg.date * 1000), - y: seg.count, - }); - } else { - motion.push({ - x: new Date(seg.date * 1000), - y: seg.count, - }); + data.forEach((seg, idx) => { + if (seg.hasObjects) { + objects.push(idx); } + + motion.push({ + x: new Date(seg.date * 1000), + y: seg.count, + }); }); graphData[hour] = { objects, motion }; @@ -316,7 +313,7 @@ export default function DesktopTimelineView({ })}
-
+
{timelineStack.playbackItems.map((timeline) => { const isSelected = timeline.range.start == selectedPlayback.range.start; @@ -386,7 +383,7 @@ export default function DesktopTimelineView({ doubleClickHandler={() => setSelectedPlayback(timeline)} /> {isSelected && graphData && ( -
+
)}