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
This commit is contained in:
Nicolas Mowen
2024-01-03 17:40:14 -06:00
committed by Blake Blackshear
parent 6dd9d54f70
commit 9c4b69191b
16 changed files with 412 additions and 28 deletions

View File

@@ -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("/<camera_name>/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("/<camera_name>/<label>/best.jpg")
@bp.route("/<camera_name>/<label>/thumbnail.jpg")
def label_thumbnail(camera_name, label):