mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 21:44:13 +03:00
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
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:
parent
6ccf8cd2b8
commit
576f692dae
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user