From 9c4b69191bfcc9ff83a6e72db92720780cb98c80 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 3 Jan 2024 17:40:14 -0600 Subject: [PATCH] Add graph showing motion and object activity to history timeline desktop view (#9184) * Add timeline graph component * Add more custom colors and improve graph * Add api and data * Fix data sorting * Add graph to timeline * Only show timeline for selected hour * Make data full range --- frigate/http.py | 86 +++++++++++- web/package-lock.json | 131 ++++++++++++++++++ web/package.json | 2 + .../components/camera/DynamicCameraImage.tsx | 6 +- web/src/components/card/HistoryCard.tsx | 2 +- web/src/components/graph/TimelineGraph.tsx | 65 +++++++++ web/src/pages/ConfigEditor.tsx | 4 +- web/src/pages/Dashboard.tsx | 2 +- web/src/pages/Export.tsx | 2 +- web/src/pages/History.tsx | 2 +- web/src/types/graph.ts | 9 ++ web/src/types/history.ts | 6 + web/src/types/record.ts | 39 ++++-- web/src/utils/historyUtil.ts | 7 +- web/src/views/history/DesktopTimelineView.tsx | 71 +++++++++- web/tailwind.config.js | 6 + 16 files changed, 412 insertions(+), 28 deletions(-) create mode 100644 web/src/components/graph/TimelineGraph.tsx create mode 100644 web/src/types/graph.ts diff --git a/frigate/http.py b/frigate/http.py index 427831b1e..4c7a01b67 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -8,6 +8,7 @@ import re import subprocess as sp import time import traceback +from collections import defaultdict from datetime import datetime, timedelta, timezone from functools import reduce from pathlib import Path @@ -620,7 +621,9 @@ def hourly_timeline(): after = request.args.get("after", type=float) limit = request.args.get("limit", 200) tz_name = request.args.get("timezone", default="utc", type=str) + _, minute_modifier, _ = get_tz_modifiers(tz_name) + minute_offset = int(minute_modifier.split(" ")[0]) clauses = [] @@ -675,7 +678,7 @@ def hourly_timeline(): minute=0, second=0, microsecond=0 ) + timedelta( - minutes=int(minute_modifier.split(" ")[0]), + minutes=minute_offset, ) ).timestamp() if hour not in hours: @@ -693,6 +696,87 @@ def hourly_timeline(): ) +@bp.route("//recording/hourly/activity") +def hourly_timeline_activity(camera_name: str): + """Get hourly summary for timeline.""" + if camera_name not in current_app.frigate_config.cameras: + return make_response( + jsonify({"success": False, "message": "Camera not found"}), + 404, + ) + + before = request.args.get("before", type=float, default=datetime.now()) + after = request.args.get( + "after", type=float, default=datetime.now() - timedelta(hours=1) + ) + tz_name = request.args.get("timezone", default="utc", type=str) + + _, minute_modifier, _ = get_tz_modifiers(tz_name) + minute_offset = int(minute_modifier.split(" ")[0]) + + all_recordings: list[Recordings] = ( + Recordings.select( + Recordings.start_time, + Recordings.duration, + Recordings.objects, + Recordings.motion, + ) + .where(Recordings.camera == camera_name) + .where(Recordings.motion > 0) + .where((Recordings.start_time > after) & (Recordings.end_time < before)) + .order_by(Recordings.start_time.asc()) + .iterator() + ) + + # data format is ex: + # {timestamp: [{ date: 1, count: 1, type: motion }]}] }} + hours: dict[int, list[dict[str, any]]] = defaultdict(list) + + key = datetime.fromtimestamp(after).replace(second=0, microsecond=0) + timedelta( + minutes=minute_offset + ) + check = (key + timedelta(hours=1)).timestamp() + + # set initial start so data is representative of full hour + hours[int(key.timestamp())].append( + { + "date": key.timestamp(), + "count": 0, + "type": "motion", + } + ) + + 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 = key + timedelta(hours=1) + check = (key + timedelta(hours=1)).timestamp() + hours[int(key.timestamp())].append( + { + "date": key.timestamp(), + "count": 0, + "type": "motion", + } + ) + + data_type = "motion" if recording.objects == 0 else "objects" + hours[int(key.timestamp())].append( + { + "date": recording.start_time + (recording.duration / 2), + "count": recording.motion, + "type": data_type, + } + ) + + return jsonify(hours) + + @bp.route("//