diff --git a/frigate/api/event.py b/frigate/api/event.py index 3b4b413be..489120b02 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -1755,3 +1755,45 @@ def delete_trigger_embedding( }, status_code=500, ) + + +@router.get( + "/triggers/status/{camera_name}", + response_model=dict, + dependencies=[Depends(require_role(["admin"]))], +) +def get_triggers_status( + camera_name: str, +): + try: + # Fetch all triggers for the specified camera + triggers = Trigger.select().where(Trigger.camera == camera_name) + + # Prepare the response with trigger status + status = { + trigger.name: { + "last_triggered": trigger.last_triggered.timestamp() + if trigger.last_triggered + else None, + "triggering_event_id": trigger.triggering_event_id + if trigger.triggering_event_id + else None, + } + for trigger in triggers + } + + if not status: + return JSONResponse( + content={ + "success": False, + "message": f"No triggers found for camera {camera_name}", + }, + status_code=404, + ) + + return {"success": True, "triggers": status} + except Exception as e: + return JSONResponse( + content={"success": False, "message": str(e)}, + status_code=500, + ) diff --git a/frigate/data_processing/post/semantic_trigger.py b/frigate/data_processing/post/semantic_trigger.py index b56ca4a56..09a096cd7 100644 --- a/frigate/data_processing/post/semantic_trigger.py +++ b/frigate/data_processing/post/semantic_trigger.py @@ -163,6 +163,13 @@ class SemanticTriggerProcessor(PostProcessorApi): f"Trigger {trigger['name']} activated with similarity {similarity:.4f}" ) + # Update the trigger's last_triggered and triggering_event_id + Trigger.update( + last_triggered=datetime.datetime.now(), triggering_event_id=event_id + ).where( + Trigger.camera == camera, Trigger.name == trigger["name"] + ).execute() + # Always publish MQTT message self.requestor.send_data( "triggers", diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 91eefd5c3..c4ae23115 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -649,7 +649,7 @@ "documentTitle": "Triggers", "management": { "title": "Trigger Management", - "desc": "Manage triggers for camera '{{camera}}' to detect specific events." + "desc": "Manage triggers for {{camera}}. Use the thumbnail type to trigger on similar thumbnails to your selected tracked object, and the description type to trigger on similar descriptions to text you specify." }, "addTrigger": "Add Trigger", "table": { diff --git a/web/src/views/settings/TriggerView.tsx b/web/src/views/settings/TriggerView.tsx index abd0c99c3..d96e26244 100644 --- a/web/src/views/settings/TriggerView.tsx +++ b/web/src/views/settings/TriggerView.tsx @@ -5,14 +5,6 @@ import useSWR from "swr"; import axios from "axios"; import { Button } from "@/components/ui/button"; import Heading from "@/components/ui/heading"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { Tooltip, @@ -20,13 +12,16 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import { LuPlus, LuTrash, LuPencil } from "react-icons/lu"; +import { LuPlus, LuTrash, LuPencil, LuSearch } from "react-icons/lu"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import CreateTriggerDialog from "@/components/overlay/CreateTriggerDialog"; import DeleteTriggerDialog from "@/components/overlay/DeleteTriggerDialog"; import { FrigateConfig } from "@/types/frigateConfig"; import { Trigger, TriggerAction, TriggerType } from "@/types/trigger"; import { useSearchEffect } from "@/hooks/use-overlay-state"; +import { cn } from "@/lib/utils"; +import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; +import { Link } from "react-router-dom"; type ConfigSetBody = { requires_restart: number; @@ -70,6 +65,7 @@ export default function TriggerView({ const { t } = useTranslation("views/settings"); const { data: config, mutate: updateConfig } = useSWR("config"); + const { data: trigger_status } = useSWR(`/triggers/status/${selectedCamera}`); const [showCreate, setShowCreate] = useState(false); const [showDelete, setShowDelete] = useState(false); const [selectedTrigger, setSelectedTrigger] = useState(null); @@ -374,118 +370,146 @@ export default function TriggerView({
-
- - - - - {t("triggers.table.name")} - - {t("triggers.table.type")} - {t("triggers.table.content")} - {t("triggers.table.threshold")} - {t("triggers.table.actions")} - - - - {triggers.length === 0 ? ( - - - {t("triggers.table.noTriggers")} - - - ) : ( - triggers.map((trigger) => ( - - +
+ {triggers.length === 0 ? ( +
+

+ {t("triggers.table.noTriggers")} +

+
+ ) : ( +
+ {triggers.map((trigger) => ( +
+
+

{trigger.name} - - - +
+
+ + {t(`triggers.type.${trigger.type}`)} + +
+ + {trigger.threshold.toFixed(2)} threshold + + {trigger.actions.length} actions + + - {t(`triggers.type.${trigger.type}`)} - - - - {trigger.type === "thumbnail" - ? trigger.data - : trigger.data.length > 30 - ? `${trigger.data.substring(0, 30)}...` - : trigger.data} - - - {trigger.threshold.toFixed(2)} - - - {trigger.actions - .map((action) => t(`triggers.actions.${action}`)) - .join(", ")} - - - -
- - - - - -

{t("triggers.table.edit")}

-
-
- - - - - -

{t("triggers.table.deleteTrigger")}

-
-
+
+ Last:{" "} + {trigger_status && + trigger_status.triggers[trigger.name] + ?.last_triggered + ? formatUnixTimestampToDateTime( + trigger_status.triggers[trigger.name] + ?.last_triggered, + { + timezone: config.ui.timezone, + date_format: + config.ui.time_format == "24hour" + ? t( + "time.formattedTimestamp2.24hour", + { + ns: "common", + }, + ) + : t( + "time.formattedTimestamp2.12hour", + { + ns: "common", + }, + ), + time_style: "medium", + date_style: "medium", + }, + ) + : "Never"} +
- - - - )) - )} - -

+ +
+
+ +
+ + + + + + +

{t("triggers.table.edit")}

+
+
+ + + + + +

{t("triggers.table.deleteTrigger")}

+
+
+
+
+
+ ))} + + )}