diff --git a/frigate/api/app.py b/frigate/api/app.py index 321bd5758..9a62080de 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -5,12 +5,9 @@ import json import logging import os import traceback -from collections import defaultdict from datetime import datetime, timedelta from functools import reduce -import numpy as np -import pandas as pd import requests from flask import ( Blueprint, @@ -632,102 +629,3 @@ def hourly_timeline(): "hours": hours, } ) - - -@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( - [ - key.timestamp(), - 0, - False, - ] - ) - - for recording in all_recordings: - if recording.start_time > check: - hours[int(key.timestamp())].append( - [ - (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( - [ - key.timestamp(), - 0, - False, - ] - ) - - data_type = recording.objects > 0 - count = recording.motion + recording.objects - hours[int(key.timestamp())].append( - [ - recording.start_time + (recording.duration / 2), - 0 if count == 0 else np.log2(count), - 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 = 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/frigate/api/review.py b/frigate/api/review.py index 0ab524580..b41d11841 100644 --- a/frigate/api/review.py +++ b/frigate/api/review.py @@ -4,6 +4,8 @@ import logging from datetime import datetime, timedelta from functools import reduce +import numpy as np +import pandas as pd from flask import ( Blueprint, jsonify, @@ -12,7 +14,7 @@ from flask import ( ) from peewee import Case, DoesNotExist, fn, operator -from frigate.models import ReviewSegment +from frigate.models import Recordings, ReviewSegment from frigate.util.builtin import get_tz_modifiers logger = logging.getLogger(__name__) @@ -258,3 +260,59 @@ def delete_reviews(ids: str): ReviewSegment.delete().where(ReviewSegment.id << list_of_ids).execute() return make_response(jsonify({"success": True, "message": "Delete reviews"}), 200) + + +@ReviewBp.route("/review/activity") +def review_activity(): + """Get motion and audio activity.""" + before = request.args.get("before", type=float, default=datetime.now().timestamp()) + after = request.args.get( + "after", type=float, default=(datetime.now() - timedelta(hours=1)).timestamp() + ) + + # get scale in seconds + scale = request.args.get( + "scale", type=int, default=30 + ) + + all_recordings: list[Recordings] = ( + Recordings.select( + Recordings.start_time, + Recordings.duration, + Recordings.objects, + Recordings.motion, + Recordings.dBFS, + ) + .where((Recordings.start_time > after) & (Recordings.end_time < before)) + .order_by(Recordings.start_time.asc()) + .iterator() + ) + + # format is: { timestamp: segment_start_ts, motion: [0-100], audio: [0 - -100] } + # periods where active objects / audio was detected will cause motion / audio to be scaled down + data: list[dict[str, float]] = [] + + for rec in all_recordings: + factor = 0.1 if rec.objects > 0 else 1.0 + data.append({ + "date": rec.start_time, + "motion": rec.motion * factor, + "audio": rec.dBFS * factor, + }) + + # resample data using pandas to get activity on scaled basis + df = pd.DataFrame(data, columns=["date", "motion", "audio"]) + + # set date as datetime index + df["date"] = pd.to_datetime(df["date"], unit="s") + df.set_index(["date"], inplace=True) + + # normalize data + df = df.resample(f"{scale}S").mean().fillna(0.0) + df["motion"] = (df["motion"] - df["motion"].min()) / (df["motion"].max() - df["motion"].min()) * 100 + df["audio"] = (df["audio"] - df["audio"].max()) / (df["audio"].min() - df["audio"].max()) * -100 + + # change types for output + df.index = df.index.astype(int) // (10**9) + normalized = df.reset_index().to_dict("records") + return jsonify(normalized)