Trigger actions (#20709)
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions

* add backend trigger actions

* config

* frontend types

* add actions to form and wizard

* i18n

* docs

* use camera level notification enabled check
This commit is contained in:
Josh Hawkins 2025-10-28 16:13:04 -05:00 committed by GitHub
parent 6ccf8cd2b8
commit 576f692dae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 88 additions and 18 deletions

View File

@ -924,10 +924,13 @@ cameras:
type: thumbnail type: thumbnail
# Reference data for matching, either an event ID for `thumbnail` or a text string for `description`. (default: none) # Reference data for matching, either an event ID for `thumbnail` or a text string for `description`. (default: none)
data: 1751565549.853251-b69j73 data: 1751565549.853251-b69j73
# Similarity threshold for triggering. (default: none) # Similarity threshold for triggering. (default: shown below)
threshold: 0.7 threshold: 0.8
# List of actions to perform when the trigger fires. (default: none) # List of actions to perform when the trigger fires. (default: none)
# Available options: `notification` (send a webpush notification) # Available options:
# - `notification` (send a webpush notification)
# - `sub_label` (add trigger friendly name as a sub label to the triggering tracked object)
# - `attribute` (add trigger's name and similarity score as a data attribute to the triggering tracked object)
actions: actions:
- notification - notification

View File

@ -119,7 +119,7 @@ Semantic Search must be enabled to use Triggers.
### Configuration ### Configuration
Triggers are defined within the `semantic_search` configuration for each camera in your Frigate configuration file or through the UI. Each trigger consists of a `friendly_name`, a `type` (either `thumbnail` or `description`), a `data` field (the reference image event ID or text), a `threshold` for similarity matching, and a list of `actions` to perform when the trigger fires. Triggers are defined within the `semantic_search` configuration for each camera in your Frigate configuration file or through the UI. Each trigger consists of a `friendly_name`, a `type` (either `thumbnail` or `description`), a `data` field (the reference image event ID or text), a `threshold` for similarity matching, and a list of `actions` to perform when the trigger fires - `notification`, `sub_label`, and `attribute`.
Triggers are best configured through the Frigate UI. Triggers are best configured through the Frigate UI.
@ -128,17 +128,20 @@ Triggers are best configured through the Frigate UI.
1. Navigate to the **Settings** page and select the **Triggers** tab. 1. Navigate to the **Settings** page and select the **Triggers** tab.
2. Choose a camera from the dropdown menu to view or manage its triggers. 2. Choose a camera from the dropdown menu to view or manage its triggers.
3. Click **Add Trigger** to create a new trigger or use the pencil icon to edit an existing one. 3. Click **Add Trigger** to create a new trigger or use the pencil icon to edit an existing one.
4. In the **Create Trigger** dialog: 4. In the **Create Trigger** wizard:
- Enter a **Name** for the trigger (e.g., "red_car_alert"). - Enter a **Name** for the trigger (e.g., "Red Car Alert").
- Enter a descriptive **Friendly Name** for the trigger (e.g., "Red car on the driveway camera"). - Enter a descriptive **Friendly Name** for the trigger (e.g., "Red car on the driveway camera").
- Select the **Type** (`Thumbnail` or `Description`). - Select the **Type** (`Thumbnail` or `Description`).
- For `Thumbnail`, select an image to trigger this action when a similar thumbnail image is detected, based on the threshold. - For `Thumbnail`, select an image to trigger this action when a similar thumbnail image is detected, based on the threshold.
- For `Description`, enter text to trigger this action when a similar tracked object description is detected. - For `Description`, enter text to trigger this action when a similar tracked object description is detected.
- Set the **Threshold** for similarity matching. - Set the **Threshold** for similarity matching.
- Select **Actions** to perform when the trigger fires. - Select **Actions** to perform when the trigger fires.
If native webpush notifications are enabled, check the `Send Notification` box to send a notification.
Check the `Add Sub Label` box to add the trigger's friendly name as a sub label to any triggering tracked objects.
Check the `Add Attribute` box to add the trigger's internal ID (e.g., "red_car_alert") to a data attribute on the tracked object that can be processed via the API or MQTT.
5. Save the trigger to update the configuration and store the embedding in the database. 5. Save the trigger to update the configuration and store the embedding in the database.
When a trigger fires, the UI highlights the trigger with a blue outline for 3 seconds for easy identification. When a trigger fires, the UI highlights the trigger with a blue dot for 3 seconds for easy identification.
### Usage and Best Practices ### Usage and Best Practices

View File

@ -33,6 +33,8 @@ class TriggerType(str, Enum):
class TriggerAction(str, Enum): class TriggerAction(str, Enum):
NOTIFICATION = "notification" NOTIFICATION = "notification"
SUB_LABEL = "sub_label"
ATTRIBUTE = "attribute"
class ObjectClassificationType(str, Enum): class ObjectClassificationType(str, Enum):

View File

@ -10,6 +10,10 @@ import cv2
import numpy as np import numpy as np
from peewee import DoesNotExist from peewee import DoesNotExist
from frigate.comms.event_metadata_updater import (
EventMetadataPublisher,
EventMetadataTypeEnum,
)
from frigate.comms.inter_process import InterProcessRequestor from frigate.comms.inter_process import InterProcessRequestor
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.const import CONFIG_DIR from frigate.const import CONFIG_DIR
@ -34,6 +38,7 @@ class SemanticTriggerProcessor(PostProcessorApi):
db: SqliteVecQueueDatabase, db: SqliteVecQueueDatabase,
config: FrigateConfig, config: FrigateConfig,
requestor: InterProcessRequestor, requestor: InterProcessRequestor,
sub_label_publisher: EventMetadataPublisher,
metrics: DataProcessorMetrics, metrics: DataProcessorMetrics,
embeddings, embeddings,
): ):
@ -41,6 +46,7 @@ class SemanticTriggerProcessor(PostProcessorApi):
self.db = db self.db = db
self.embeddings = embeddings self.embeddings = embeddings
self.requestor = requestor self.requestor = requestor
self.sub_label_publisher = sub_label_publisher
self.trigger_embeddings: list[np.ndarray] = [] self.trigger_embeddings: list[np.ndarray] = []
self.thumb_stats = ZScoreNormalization() self.thumb_stats = ZScoreNormalization()
@ -184,14 +190,44 @@ class SemanticTriggerProcessor(PostProcessorApi):
), ),
) )
friendly_name = (
self.config.cameras[camera]
.semantic_search.triggers[trigger["name"]]
.friendly_name
)
if ( if (
self.config.cameras[camera] self.config.cameras[camera]
.semantic_search.triggers[trigger["name"]] .semantic_search.triggers[trigger["name"]]
.actions .actions
): ):
# TODO: handle actions for the trigger # handle actions for the trigger
# notifications already handled by webpush # notifications already handled by webpush
pass if (
"sub_label"
in self.config.cameras[camera]
.semantic_search.triggers[trigger["name"]]
.actions
):
self.sub_label_publisher.publish(
(event_id, friendly_name, similarity),
EventMetadataTypeEnum.sub_label,
)
if (
"attribute"
in self.config.cameras[camera]
.semantic_search.triggers[trigger["name"]]
.actions
):
self.sub_label_publisher.publish(
(
event_id,
trigger["name"],
trigger["type"],
similarity,
),
EventMetadataTypeEnum.attribute.value,
)
if WRITE_DEBUG_IMAGES: if WRITE_DEBUG_IMAGES:
try: try:

View File

@ -233,6 +233,7 @@ class EmbeddingMaintainer(threading.Thread):
db, db,
self.config, self.config,
self.requestor, self.requestor,
self.event_metadata_publisher,
metrics, metrics,
self.embeddings, self.embeddings,
) )

View File

@ -903,8 +903,9 @@
"description": "Description" "description": "Description"
}, },
"actions": { "actions": {
"alert": "Mark as Alert", "notification": "Send Notification",
"notification": "Send Notification" "sub_label": "Add Sub Label",
"attribute": "Add Attribute"
}, },
"dialog": { "dialog": {
"createTrigger": { "createTrigger": {
@ -959,7 +960,7 @@
}, },
"actions": { "actions": {
"title": "Actions", "title": "Actions",
"desc": "By default, Frigate fires an MQTT message for all triggers. Choose an additional action to perform when this trigger fires.", "desc": "By default, Frigate fires an MQTT message for all triggers. Sub labels add the trigger name to the object label. Attributes are searchable metadata stored separately in the tracked object metadata.",
"error": { "error": {
"min": "At least one action must be selected." "min": "At least one action must be selected."
} }

View File

@ -79,6 +79,15 @@ export default function CreateTriggerDialog({
const { t } = useTranslation("views/settings"); const { t } = useTranslation("views/settings");
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const availableActions = useMemo(() => {
if (!config) return [];
if (config.cameras[selectedCamera].notifications.enabled_in_config) {
return ["notification", "sub_label", "attribute"];
}
return ["sub_label", "attribute"];
}, [config, selectedCamera]);
const existingTriggerNames = useMemo(() => { const existingTriggerNames = useMemo(() => {
if ( if (
!config || !config ||
@ -132,7 +141,7 @@ export default function CreateTriggerDialog({
.number() .number()
.min(0, t("triggers.dialog.form.threshold.error.min")) .min(0, t("triggers.dialog.form.threshold.error.min"))
.max(1, t("triggers.dialog.form.threshold.error.max")), .max(1, t("triggers.dialog.form.threshold.error.max")),
actions: z.array(z.enum(["notification"])), actions: z.array(z.enum(["notification", "sub_label", "attribute"])),
}); });
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
@ -383,7 +392,7 @@ export default function CreateTriggerDialog({
{t("triggers.dialog.form.actions.title")} {t("triggers.dialog.form.actions.title")}
</FormLabel> </FormLabel>
<div className="space-y-2"> <div className="space-y-2">
{["notification"].map((action) => ( {availableActions.map((action) => (
<div key={action} className="flex items-center space-x-2"> <div key={action} className="flex items-center space-x-2">
<FormControl> <FormControl>
<Checkbox <Checkbox

View File

@ -243,6 +243,7 @@ export default function TriggerWizardDialog({
<Step3ThresholdAndActions <Step3ThresholdAndActions
initialData={wizardState.step3Data} initialData={wizardState.step3Data}
trigger={trigger} trigger={trigger}
camera={selectedCamera}
onNext={handleStep3Next} onNext={handleStep3Next}
onBack={handleBack} onBack={handleBack}
isLoading={isLoading} isLoading={isLoading}

View File

@ -1,4 +1,4 @@
import { useEffect, useCallback } from "react"; import { useEffect, useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@ -17,6 +17,8 @@ import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Trigger, TriggerAction } from "@/types/trigger"; import { Trigger, TriggerAction } from "@/types/trigger";
import ActivityIndicator from "@/components/indicators/activity-indicator"; import ActivityIndicator from "@/components/indicators/activity-indicator";
import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
export type Step3FormData = { export type Step3FormData = {
threshold: number; threshold: number;
@ -26,6 +28,7 @@ export type Step3FormData = {
type Step3ThresholdAndActionsProps = { type Step3ThresholdAndActionsProps = {
initialData?: Step3FormData; initialData?: Step3FormData;
trigger?: Trigger | null; trigger?: Trigger | null;
camera: string;
onNext: (data: Step3FormData) => void; onNext: (data: Step3FormData) => void;
onBack: () => void; onBack: () => void;
isLoading?: boolean; isLoading?: boolean;
@ -34,18 +37,29 @@ type Step3ThresholdAndActionsProps = {
export default function Step3ThresholdAndActions({ export default function Step3ThresholdAndActions({
initialData, initialData,
trigger, trigger,
camera,
onNext, onNext,
onBack, onBack,
isLoading = false, isLoading = false,
}: Step3ThresholdAndActionsProps) { }: Step3ThresholdAndActionsProps) {
const { t } = useTranslation("views/settings"); const { t } = useTranslation("views/settings");
const { data: config } = useSWR<FrigateConfig>("config");
const availableActions = useMemo(() => {
if (!config) return [];
if (config.cameras[camera].notifications.enabled_in_config) {
return ["notification", "sub_label", "attribute"];
}
return ["sub_label", "attribute"];
}, [config, camera]);
const formSchema = z.object({ const formSchema = z.object({
threshold: z threshold: z
.number() .number()
.min(0, t("triggers.dialog.form.threshold.error.min")) .min(0, t("triggers.dialog.form.threshold.error.min"))
.max(1, t("triggers.dialog.form.threshold.error.max")), .max(1, t("triggers.dialog.form.threshold.error.max")),
actions: z.array(z.enum(["notification"])), actions: z.array(z.enum(["notification", "sub_label", "attribute"])),
}); });
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
@ -127,7 +141,7 @@ export default function Step3ThresholdAndActions({
<FormItem> <FormItem>
<FormLabel>{t("triggers.dialog.form.actions.title")}</FormLabel> <FormLabel>{t("triggers.dialog.form.actions.title")}</FormLabel>
<div className="space-y-2"> <div className="space-y-2">
{["notification"].map((action) => ( {availableActions.map((action) => (
<div key={action} className="flex items-center space-x-2"> <div key={action} className="flex items-center space-x-2">
<FormControl> <FormControl>
<Checkbox <Checkbox

View File

@ -1,5 +1,5 @@
export type TriggerType = "thumbnail" | "description"; export type TriggerType = "thumbnail" | "description";
export type TriggerAction = "notification"; export type TriggerAction = "notification" | "sub_label" | "attribute";
export type Trigger = { export type Trigger = {
enabled: boolean; enabled: boolean;