diff --git a/frigate/http.py b/frigate/http.py index 427831b1e..21e5018b7 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,85 @@ 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)) + .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() + + for recording in all_recordings: + if recording.start_time > check: + key = key + timedelta(hours=1) + check = (key + timedelta(hours=1)).timestamp() + + 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, + } + ) + + # process data to make data counts relative + for hour, data in hours.items(): + motion_values = np.asarray(list(map(lambda m: m["count"], data))) + avg = motion_values.mean() + std = motion_values.std() + + for idx, motion in enumerate(motion_values): + if motion < (avg - (std * 2)): + value = 1 + elif motion < (avg - std): + value = 2 + elif motion < avg: + value = 3 + elif motion < (avg + std): + value = 4 + elif motion < (avg + (std * 2)): + value = 5 + else: + value = 6 + + hours[hour][idx]["count"] = value + + return jsonify(hours) + + @bp.route("//