Convert events endpoints to FastAPI

This commit is contained in:
Rui Alves 2024-09-16 10:44:03 +01:00
parent cc259736da
commit 1b601b8c35
8 changed files with 330 additions and 230 deletions

View File

@ -21,7 +21,6 @@ from werkzeug.middleware.proxy_fix import ProxyFix
from frigate.api.auth import AuthBp, get_jwt_secret, limiter
from frigate.api.defs.tags import Tags
from frigate.api.event import EventBp
from frigate.config import FrigateConfig
from frigate.const import CONFIG_DIR
from frigate.embeddings import EmbeddingsContext
@ -43,7 +42,6 @@ logger = logging.getLogger(__name__)
bp = Blueprint("frigate", __name__)
bp.register_blueprint(EventBp)
bp.register_blueprint(AuthBp)
router = APIRouter()

View File

@ -0,0 +1,52 @@
from typing import Optional
from pydantic import BaseModel
DEFAULT_TIME_RANGE = "00:00,24:00"
class EventsQueryParams(BaseModel):
camera: Optional[str] = "all"
cameras: Optional[str] = "all"
label: Optional[str] = "all"
labels: Optional[str] = "all"
sub_label: Optional[str] = "all"
sub_labels: Optional[str] = "all"
zone: Optional[str] = "all"
zones: Optional[str] = "all"
limit: Optional[int] = 100
after: Optional[float] = None
before: Optional[float] = None
time_range: Optional[str] = DEFAULT_TIME_RANGE
has_clip: Optional[int] = None
has_snapshot: Optional[int] = None
in_progress: Optional[int] = None
include_thumbnails: Optional[int] = 1
favorites: Optional[int] = None
min_score: Optional[float] = None
max_score: Optional[float] = None
is_submitted: Optional[int] = None
min_length: Optional[float] = None
max_length: Optional[float] = None
sort: Optional[str] = None
timezone: Optional[str] = "utc"
class EventsSearchQueryParams(BaseModel):
query: Optional[str] = None
search_type: Optional[str] = "thumbnail,description"
include_thumbnails: Optional[int] = 1
limit: Optional[int] = 50
cameras: Optional[str] = "all"
labels: Optional[str] = "all"
zones: Optional[str] = "all"
after: Optional[float] = None
before: Optional[float] = None
timezone: Optional[str] = "utc"
class EventsSummaryQueryParams(BaseModel):
timezone: Optional[str] = "utc"
has_clip: Optional[int] = None
has_snapshot: Optional[int] = None

View File

@ -9,3 +9,4 @@ class Tags(Enum):
notifications = "Notifications"
review = "Review"
export = "Export"
events = "Events"

View File

@ -11,17 +11,20 @@ from urllib.parse import unquote
import cv2
import numpy as np
from flask import (
Blueprint,
current_app,
jsonify,
make_response,
request,
)
from fastapi import APIRouter, Request
from fastapi.params import Depends
from fastapi.responses import JSONResponse
from peewee import JOIN, DoesNotExist, fn, operator
from PIL import Image
from playhouse.shortcuts import model_to_dict
from frigate.api.defs.events_query_parameters import (
DEFAULT_TIME_RANGE,
EventsQueryParams,
EventsSearchQueryParams,
EventsSummaryQueryParams,
)
from frigate.api.defs.tags import Tags
from frigate.const import (
CLIPS_DIR,
)
@ -33,57 +36,55 @@ from frigate.util.builtin import get_tz_modifiers
logger = logging.getLogger(__name__)
EventBp = Blueprint("events", __name__)
DEFAULT_TIME_RANGE = "00:00,24:00"
router = APIRouter(tags=[Tags.events])
@EventBp.route("/events")
def events():
camera = request.args.get("camera", "all")
cameras = request.args.get("cameras", "all")
@router.get("/events")
def events(params: EventsQueryParams = Depends()):
camera = params.camera
cameras = params.cameras
# handle old camera arg
if cameras == "all" and camera != "all":
cameras = camera
label = unquote(request.args.get("label", "all"))
labels = request.args.get("labels", "all")
label = unquote(params.label)
labels = params.labels
# handle old label arg
if labels == "all" and label != "all":
labels = label
sub_label = request.args.get("sub_label", "all")
sub_labels = request.args.get("sub_labels", "all")
sub_label = params.sub_label
sub_labels = params.sub_labels
# handle old sub_label arg
if sub_labels == "all" and sub_label != "all":
sub_labels = sub_label
zone = request.args.get("zone", "all")
zones = request.args.get("zones", "all")
zone = params.zone
zones = params.zones
# handle old label arg
if zones == "all" and zone != "all":
zones = zone
limit = request.args.get("limit", 100)
after = request.args.get("after", type=float)
before = request.args.get("before", type=float)
time_range = request.args.get("time_range", DEFAULT_TIME_RANGE)
has_clip = request.args.get("has_clip", type=int)
has_snapshot = request.args.get("has_snapshot", type=int)
in_progress = request.args.get("in_progress", type=int)
include_thumbnails = request.args.get("include_thumbnails", default=1, type=int)
favorites = request.args.get("favorites", type=int)
min_score = request.args.get("min_score", type=float)
max_score = request.args.get("max_score", type=float)
is_submitted = request.args.get("is_submitted", type=int)
min_length = request.args.get("min_length", type=float)
max_length = request.args.get("max_length", type=float)
limit = params.limit
after = params.after
before = params.before
time_range = params.time_range
has_clip = params.has_clip
has_snapshot = params.has_snapshot
in_progress = params.in_progress
include_thumbnails = params.include_thumbnails
favorites = params.favorites
min_score = params.min_score
max_score = params.max_score
is_submitted = params.is_submitted
min_length = params.min_length
max_length = params.max_length
sort = request.args.get("sort", type=str)
sort = params.sort
clauses = []
@ -163,7 +164,7 @@ def events():
if time_range != DEFAULT_TIME_RANGE:
# get timezone arg to ensure browser times are used
tz_name = request.args.get("timezone", default="utc", type=str)
tz_name = params.timezone
hour_modifier, minute_modifier, _ = get_tz_modifiers(tz_name)
times = time_range.split(",")
@ -248,13 +249,11 @@ def events():
.iterator()
)
return jsonify(list(events))
return JSONResponse(content=list(events))
@EventBp.route("/events/explore")
def events_explore():
limit = request.args.get("limit", 10, type=int)
@router.get("/events/explore")
def events_explore(limit: int = 10):
subquery = Event.select(
Event.id,
Event.camera,
@ -316,66 +315,65 @@ def events_explore():
for event in events
]
return jsonify(processed_events)
return JSONResponse(content=processed_events)
@EventBp.route("/event_ids")
def event_ids():
idString = request.args.get("ids")
ids = idString.split(",")
@router.get("/event_ids")
def event_ids(ids: str):
ids = ids.split(",")
if not ids:
return make_response(
jsonify({"success": False, "message": "Valid list of ids must be sent"}),
400,
return JSONResponse(
content=({"success": False, "message": "Valid list of ids must be sent"}),
status_code=400,
)
try:
events = Event.select().where(Event.id << ids).dicts().iterator()
return jsonify(list(events))
return JSONResponse(list(events))
except Exception:
return make_response(
jsonify({"success": False, "message": "Events not found"}), 400
return JSONResponse(
content=({"success": False, "message": "Events not found"}), status_code=400
)
@EventBp.route("/events/search")
def events_search():
query = request.args.get("query", type=str)
search_type = request.args.get("search_type", "thumbnail,description", type=str)
include_thumbnails = request.args.get("include_thumbnails", default=1, type=int)
limit = request.args.get("limit", 50, type=int)
@router.get("/events/search")
def events_search(request: Request, params: EventsSearchQueryParams = Depends()):
query = params.query
search_type = params.search_type
include_thumbnails = params.include_thumbnails
limit = params.limit
# Filters
cameras = request.args.get("cameras", "all", type=str)
labels = request.args.get("labels", "all", type=str)
zones = request.args.get("zones", "all", type=str)
after = request.args.get("after", type=float)
before = request.args.get("before", type=float)
cameras = params.cameras
labels = params.labels
zones = params.zones
after = params.after
before = params.before
if not query:
return make_response(
jsonify(
return JSONResponse(
content=(
{
"success": False,
"message": "A search query must be supplied",
}
),
400,
status_code=400,
)
if not current_app.frigate_config.semantic_search.enabled:
return make_response(
jsonify(
if not request.app.frigate_config.semantic_search.enabled:
return JSONResponse(
content=(
{
"success": False,
"message": "Semantic search is not enabled",
}
),
400,
status_code=400,
)
context: EmbeddingsContext = current_app.embeddings
context: EmbeddingsContext = request.app.embeddings
selected_columns = [
Event.id,
@ -434,14 +432,14 @@ def events_search():
try:
search_event: Event = Event.get(Event.id == query)
except DoesNotExist:
return make_response(
jsonify(
return JSONResponse(
content=(
{
"success": False,
"message": "Event not found",
}
),
404,
status_code=404,
)
thumbnail = base64.b64decode(search_event.thumbnail)
img = np.array(Image.open(io.BytesIO(thumbnail)).convert("RGB"))
@ -501,7 +499,7 @@ def events_search():
}
if not results:
return jsonify([])
return JSONResponse(content=[])
# Get the event data
events = (
@ -534,15 +532,15 @@ def events_search():
]
events = sorted(events, key=lambda x: x["search_distance"])[:limit]
return jsonify(events)
return JSONResponse(content=events)
@EventBp.route("/events/summary")
def events_summary():
tz_name = request.args.get("timezone", default="utc", type=str)
@router.get("/events/summary")
def events_summary(params: EventsSummaryQueryParams = Depends()):
tz_name = params.timezone
hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(tz_name)
has_clip = request.args.get("has_clip", type=int)
has_snapshot = request.args.get("has_snapshot", type=int)
has_clip = params.has_clip
has_snapshot = params.has_snapshot
clauses = []
@ -579,47 +577,49 @@ def events_summary():
)
)
return jsonify([e for e in groups.dicts()])
return JSONResponse(content=[e for e in groups.dicts()])
@EventBp.route("/events/<id>", methods=("GET",))
def event(id):
@router.get("/events/{event_id}")
def event(event_id: str):
try:
return model_to_dict(Event.get(Event.id == id))
return model_to_dict(Event.get(Event.id == event_id))
except DoesNotExist:
return "Event not found", 404
@EventBp.route("/events/<id>/retain", methods=("POST",))
def set_retain(id):
@router.post("/events/{event_id}/retain")
def set_retain(event_id: str):
try:
event = Event.get(Event.id == id)
event = Event.get(Event.id == event_id)
except DoesNotExist:
return make_response(
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
return JSONResponse(
content=({"success": False, "message": "Event " + event_id + " not found"}),
status_code=404,
)
event.retain_indefinitely = True
event.save()
return make_response(
jsonify({"success": True, "message": "Event " + id + " retained"}), 200
return JSONResponse(
content=({"success": True, "message": "Event " + event_id + " retained"}),
status_code=200,
)
@EventBp.route("/events/<id>/plus", methods=("POST",))
def send_to_plus(id):
if not current_app.plus_api.is_active():
@router.post("/events/{event_id}/plus")
def send_to_plus(request: Request, event_id: str):
if not request.app.plus_api.is_active():
message = "PLUS_API_KEY environment variable is not set"
logger.error(message)
return make_response(
jsonify(
return JSONResponse(
content=(
{
"success": False,
"message": message,
}
),
400,
status_code=400,
)
include_annotation = (
@ -627,11 +627,13 @@ def send_to_plus(id):
)
try:
event = Event.get(Event.id == id)
event = Event.get(Event.id == event_id)
except DoesNotExist:
message = f"Event {id} not found"
message = f"Event {event_id} not found"
logger.error(message)
return make_response(jsonify({"success": False, "message": message}), 404)
return JSONResponse(
content=({"success": False, "message": message}), status_code=404
)
# events from before the conversion to relative dimensions cant include annotations
if event.data.get("box") is None:
@ -639,20 +641,22 @@ def send_to_plus(id):
if event.end_time is None:
logger.error(f"Unable to load clean png for in-progress event: {event.id}")
return make_response(
jsonify(
return JSONResponse(
content=(
{
"success": False,
"message": "Unable to load clean png for in-progress event",
}
),
400,
status_code=400,
)
if event.plus_id:
message = "Already submitted to plus"
logger.error(message)
return make_response(jsonify({"success": False, "message": message}), 400)
return JSONResponse(
content=({"success": False, "message": message}), status_code=400
)
# load clean.png
try:
@ -660,29 +664,29 @@ def send_to_plus(id):
image = cv2.imread(os.path.join(CLIPS_DIR, filename))
except Exception:
logger.error(f"Unable to load clean png for event: {event.id}")
return make_response(
jsonify(
return JSONResponse(
content=(
{"success": False, "message": "Unable to load clean png for event"}
),
400,
status_code=400,
)
if image is None or image.size == 0:
logger.error(f"Unable to load clean png for event: {event.id}")
return make_response(
jsonify(
return JSONResponse(
content=(
{"success": False, "message": "Unable to load clean png for event"}
),
400,
status_code=400,
)
try:
plus_id = current_app.plus_api.upload_image(image, event.camera)
plus_id = request.app.plus_api.upload_image(image, event.camera)
except Exception as ex:
logger.exception(ex)
return make_response(
jsonify({"success": False, "message": "Error uploading image"}),
400,
return JSONResponse(
content=({"success": False, "message": "Error uploading image"}),
status_code=400,
)
# store image id in the database
@ -693,7 +697,7 @@ def send_to_plus(id):
box = event.data["box"]
try:
current_app.plus_api.add_annotation(
request.app.plus_api.add_annotation(
event.plus_id,
box,
event.label,
@ -701,59 +705,67 @@ def send_to_plus(id):
except ValueError:
message = "Error uploading annotation, unsupported label provided."
logger.error(message)
return make_response(
jsonify({"success": False, "message": message}),
400,
return JSONResponse(
content=({"success": False, "message": message}),
status_code=400,
)
except Exception as ex:
logger.exception(ex)
return make_response(
jsonify({"success": False, "message": "Error uploading annotation"}),
400,
return JSONResponse(
content=({"success": False, "message": "Error uploading annotation"}),
status_code=400,
)
return make_response(jsonify({"success": True, "plus_id": plus_id}), 200)
return JSONResponse(
content=({"success": True, "plus_id": plus_id}), status_code=200
)
@EventBp.route("/events/<id>/false_positive", methods=("PUT",))
def false_positive(id):
if not current_app.plus_api.is_active():
@router.put("/events/{event_id}/false_positive")
def false_positive(request: Request, event_id: str):
if not request.app.plus_api.is_active():
message = "PLUS_API_KEY environment variable is not set"
logger.error(message)
return make_response(
jsonify(
return JSONResponse(
content=(
{
"success": False,
"message": message,
}
),
400,
status_code=400,
)
try:
event = Event.get(Event.id == id)
event = Event.get(Event.id == event_id)
except DoesNotExist:
message = f"Event {id} not found"
message = f"Event {event_id} not found"
logger.error(message)
return make_response(jsonify({"success": False, "message": message}), 404)
return JSONResponse(
content=({"success": False, "message": message}), status_code=404
)
# events from before the conversion to relative dimensions cant include annotations
if event.data.get("box") is None:
message = "Events prior to 0.13 cannot be submitted as false positives"
logger.error(message)
return make_response(jsonify({"success": False, "message": message}), 400)
return JSONResponse(
content=({"success": False, "message": message}), status_code=400
)
if event.false_positive:
message = "False positive already submitted to Frigate+"
logger.error(message)
return make_response(jsonify({"success": False, "message": message}), 400)
return JSONResponse(
content=({"success": False, "message": message}), status_code=400
)
if not event.plus_id:
plus_response = send_to_plus(id)
plus_response = send_to_plus(event_id)
if plus_response.status_code != 200:
return plus_response
# need to refetch the event now that it has a plus_id
event = Event.get(Event.id == id)
event = Event.get(Event.id == event_id)
region = event.data["region"]
box = event.data["box"]
@ -766,7 +778,7 @@ def false_positive(id):
)
try:
current_app.plus_api.add_false_positive(
request.app.plus_api.add_false_positive(
event.plus_id,
region,
box,
@ -779,92 +791,101 @@ def false_positive(id):
except ValueError:
message = "Error uploading false positive, unsupported label provided."
logger.error(message)
return make_response(
jsonify({"success": False, "message": message}),
400,
return JSONResponse(
content=({"success": False, "message": message}),
status_code=400,
)
except Exception as ex:
logger.exception(ex)
return make_response(
jsonify({"success": False, "message": "Error uploading false positive"}),
400,
return JSONResponse(
content=({"success": False, "message": "Error uploading false positive"}),
status_code=400,
)
event.false_positive = True
event.save()
return make_response(jsonify({"success": True, "plus_id": event.plus_id}), 200)
return JSONResponse(
content=({"success": True, "plus_id": event.plus_id}), status_code=200
)
@EventBp.route("/events/<id>/retain", methods=("DELETE",))
def delete_retain(id):
@router.delete("/events/{event_id}/retain")
def delete_retain(event_id: str):
try:
event = Event.get(Event.id == id)
event = Event.get(Event.id == event_id)
except DoesNotExist:
return make_response(
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
return JSONResponse(
content=({"success": False, "message": "Event " + event_id + " not found"}),
status_code=404,
)
event.retain_indefinitely = False
event.save()
return make_response(
jsonify({"success": True, "message": "Event " + id + " un-retained"}), 200
return JSONResponse(
content=({"success": True, "message": "Event " + event_id + " un-retained"}),
status_code=200,
)
@EventBp.route("/events/<id>/sub_label", methods=("POST",))
def set_sub_label(id):
@router.post("/events/{event_id}/sub_label")
def set_sub_label(
request: Request,
event_id: str,
body: dict = None,
):
try:
event: Event = Event.get(Event.id == id)
event: Event = Event.get(Event.id == event_id)
except DoesNotExist:
return make_response(
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
return JSONResponse(
content=({"success": False, "message": "Event " + event_id + " not found"}),
status_code=404,
)
json: dict[str, any] = request.get_json(silent=True) or {}
json: dict[str, any] = body or {}
new_sub_label = json.get("subLabel")
new_score = json.get("subLabelScore")
if new_sub_label is None:
return make_response(
jsonify(
return JSONResponse(
content=(
{
"success": False,
"message": "A sub label must be supplied",
}
),
400,
status_code=400,
)
if new_sub_label and len(new_sub_label) > 100:
return make_response(
jsonify(
return JSONResponse(
content=(
{
"success": False,
"message": new_sub_label
+ " exceeds the 100 character limit for sub_label",
}
),
400,
status_code=400,
)
if new_score is not None and (new_score > 1.0 or new_score < 0):
return make_response(
jsonify(
return JSONResponse(
content=(
{
"success": False,
"message": new_score
+ " does not fit within the expected bounds 0 <= score <= 1.0",
}
),
400,
status_code=400,
)
if not event.end_time:
# update tracked object
tracked_obj: TrackedObject = (
current_app.detected_frames_processor.camera_states[
request.app.detected_frames_processor.camera_states[
event.camera
].tracked_objects.get(event.id)
)
@ -875,7 +896,7 @@ def set_sub_label(id):
# update timeline items
Timeline.update(
data=Timeline.data.update({"sub_label": (new_sub_label, new_score)})
).where(Timeline.source_id == id).execute()
).where(Timeline.source_id == event_id).execute()
event.sub_label = new_sub_label
@ -885,70 +906,79 @@ def set_sub_label(id):
event.data = data
event.save()
return make_response(
jsonify(
return JSONResponse(
content=(
{
"success": True,
"message": "Event " + id + " sub label set to " + new_sub_label,
"message": "Event " + event_id + " sub label set to " + new_sub_label,
}
),
200,
status_code=200,
)
@EventBp.route("/events/<id>/description", methods=("POST",))
def set_description(id):
@router.post("/events/{event_id}/description")
def set_description(
request: Request,
event_id: str,
body: dict = None,
):
try:
event: Event = Event.get(Event.id == id)
event: Event = Event.get(Event.id == event_id)
except DoesNotExist:
return make_response(
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
return JSONResponse(
content=({"success": False, "message": "Event " + event_id + " not found"}),
status_code=404,
)
json: dict[str, any] = request.get_json(silent=True) or {}
json: dict[str, any] = body or {}
new_description = json.get("description")
if new_description is None or len(new_description) == 0:
return make_response(
jsonify(
return JSONResponse(
content=(
{
"success": False,
"message": "description cannot be empty",
}
),
400,
status_code=400,
)
event.data["description"] = new_description
event.save()
# If semantic search is enabled, update the index
if current_app.frigate_config.semantic_search.enabled:
context: EmbeddingsContext = current_app.embeddings
if request.app.frigate_config.semantic_search.enabled:
context: EmbeddingsContext = request.app.embeddings
context.embeddings.description.upsert(
documents=[new_description],
metadatas=[get_metadata(event)],
ids=[id],
ids=[event_id],
)
return make_response(
jsonify(
return JSONResponse(
content=(
{
"success": True,
"message": "Event " + id + " description set to " + new_description,
"message": "Event "
+ event_id
+ " description set to "
+ new_description,
}
),
200,
status_code=200,
)
@EventBp.route("/events/<id>", methods=("DELETE",))
def delete_event(id):
@router.delete("/events/{event_id}")
def delete_event(request: Request, event_id: str):
try:
event = Event.get(Event.id == id)
event = Event.get(Event.id == event_id)
except DoesNotExist:
return make_response(
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
return JSONResponse(
content=({"success": False, "message": "Event " + event_id + " not found"}),
status_code=404,
)
media_name = f"{event.camera}-{event.id}"
@ -962,40 +992,48 @@ def delete_event(id):
media.unlink(missing_ok=True)
event.delete_instance()
Timeline.delete().where(Timeline.source_id == id).execute()
Timeline.delete().where(Timeline.source_id == event_id).execute()
# If semantic search is enabled, update the index
if current_app.frigate_config.semantic_search.enabled:
context: EmbeddingsContext = current_app.embeddings
context.embeddings.thumbnail.delete(ids=[id])
context.embeddings.description.delete(ids=[id])
return make_response(
jsonify({"success": True, "message": "Event " + id + " deleted"}), 200
if request.app.frigate_config.semantic_search.enabled:
context: EmbeddingsContext = request.app.embeddings
context.embeddings.thumbnail.delete(ids=[event_id])
context.embeddings.description.delete(ids=[event_id])
return JSONResponse(
content=({"success": True, "message": "Event " + event_id + " deleted"}),
status_code=200,
)
@EventBp.route("/events/<camera_name>/<label>/create", methods=["POST"])
def create_event(camera_name, label):
if not camera_name or not current_app.frigate_config.cameras.get(camera_name):
return make_response(
jsonify(
@router.post("/events/{camera_name}/{label}/create")
def create_event(
request: Request,
camera_name: str,
label: str,
body: dict = None,
):
if not camera_name or not request.app.frigate_config.cameras.get(camera_name):
return JSONResponse(
content=(
{"success": False, "message": f"{camera_name} is not a valid camera."}
),
404,
status_code=404,
)
if not label:
return make_response(
jsonify({"success": False, "message": f"{label} must be set."}), 404
return JSONResponse(
content=({"success": False, "message": f"{label} must be set."}),
status_code=404,
)
json: dict[str, any] = request.get_json(silent=True) or {}
json: dict[str, any] = body or {}
try:
frame = current_app.detected_frames_processor.get_current_frame(camera_name)
frame = request.app.detected_frames_processor.get_current_frame(camera_name)
event_id = current_app.external_processor.create_manual_event(
event_id = request.app.external_processor.create_manual_event(
camera_name,
label,
# TODO: Create body model
json.get("source_type", "api"),
json.get("sub_label", None),
json.get("score", 0),
@ -1006,38 +1044,39 @@ def create_event(camera_name, label):
)
except Exception as e:
logger.error(e)
return make_response(
jsonify({"success": False, "message": "An unknown error occurred"}),
500,
return JSONResponse(
content=({"success": False, "message": "An unknown error occurred"}),
status_code=500,
)
return make_response(
jsonify(
return JSONResponse(
content=(
{
"success": True,
"message": "Successfully created event.",
"event_id": event_id,
}
),
200,
status_code=200,
)
@EventBp.route("/events/<event_id>/end", methods=["PUT"])
def end_event(event_id):
json: dict[str, any] = request.get_json(silent=True) or {}
@router.put("/events/{event_id}/end")
def end_event(request: Request, event_id: str, body: dict):
json: dict[str, any] = body or {}
try:
end_time = json.get("end_time", datetime.now().timestamp())
current_app.external_processor.finish_manual_event(event_id, end_time)
request.app.external_processor.finish_manual_event(event_id, end_time)
except Exception:
return make_response(
jsonify(
return JSONResponse(
content=(
{"success": False, "message": f"{event_id} must be set and valid."}
),
404,
status_code=404,
)
return make_response(
jsonify({"success": True, "message": "Event successfully ended."}), 200
return JSONResponse(
content=({"success": True, "message": "Event successfully ended."}),
status_code=200,
)

View File

@ -1,9 +1,12 @@
import logging
from typing import Optional
from fastapi import FastAPI
from frigate.api import app as main_app
from frigate.api import export, media, notification, preview, review
from frigate.embeddings import EmbeddingsContext
from frigate.events.external import ExternalEventProcessor
from frigate.plus import PlusApi
from frigate.ptz.onvif import OnvifController
from frigate.stats.emitter import StatsEmitter
@ -14,11 +17,13 @@ logger = logging.getLogger(__name__)
def create_fastapi_app(
frigate_config,
embeddings: Optional[EmbeddingsContext],
detected_frames_processor,
storage_maintainer: StorageMaintainer,
onvif: OnvifController,
plus_api: PlusApi,
stats_emitter: StatsEmitter,
external_processor: ExternalEventProcessor,
):
logger.info("Starting FastAPI app")
app = FastAPI(
@ -34,11 +39,13 @@ def create_fastapi_app(
app.include_router(export.router)
# App Properties
app.frigate_config = frigate_config
app.embeddings = embeddings
app.detected_frames_processor = detected_frames_processor
app.storage_maintainer = storage_maintainer
app.camera_error_image = None
app.onvif = onvif
app.plus_api = plus_api
app.stats_emitter = stats_emitter
app.external_processor = external_processor
return app

View File

@ -529,7 +529,7 @@ def motion_activity(params: ReviewActivityMotionQueryParams = Depends()):
# change types for output
df.index = df.index.astype(int) // (10**9)
normalized = df.reset_index().to_dict("records")
return jsonify(normalized)
return JSONResponse(content=normalized)
@router.get("/review/activity/audio")
@ -590,4 +590,4 @@ def audio_activity(params: ReviewActivityMotionQueryParams = Depends()):
# change types for output
df.index = df.index.astype(int) // (10**9)
normalized = df.reset_index().to_dict("records")
return jsonify(normalized)
return JSONResponse(content=normalized)

View File

@ -402,11 +402,13 @@ class FrigateApp:
self.fastapi_app = create_fastapi_app(
self.config,
self.embeddings,
self.detected_frames_processor,
self.storage_maintainer,
self.onvif_controller,
self.plus_api,
self.stats_emitter,
self.external_event_processor,
)
def init_onvif(self) -> None:

View File

@ -369,6 +369,7 @@ class TestHttp(unittest.TestCase):
None,
None,
None,
None,
PlusApi(),
None,
)