Skip to content

Commit

Permalink
Merge pull request #202 from 0x2142/fix-review-multievent
Browse files Browse the repository at this point in the history
Fix issue with alerts for multi-event review items
  • Loading branch information
0x2142 authored Feb 3, 2025
2 parents ef457bf + b513814 commit e03865e
Show file tree
Hide file tree
Showing 12 changed files with 131 additions and 70 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023-2024 Matt Schmitz
Copyright (c) 2023-2025 Matt Schmitz

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
2 changes: 1 addition & 1 deletion api/v1/notiftest.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func PostNotifTest(ctx context.Context, input *struct{}) (*NotifTestOutput, erro
json.Unmarshal([]byte(response), &events)

// Send test notification
notifier.SendAlert(events[0])
notifier.SendAlert(events)
}()

log.Trace().
Expand Down
2 changes: 1 addition & 1 deletion config/internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ var Internal models.InternalConfig

func init() {
Internal.Status.Notifications.Enabled = true
Internal.AppVersion = "v0.4.0"
Internal.AppVersion = "v0.4.1-dev"
Internal.Status.Health = "n/a"
Internal.Status.API = "n/a"
Internal.Status.Frigate.API = "n/a"
Expand Down
3 changes: 3 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## [v0.4.1](https://github.com/0x2142/frigate-notify/releases/tag/v0.4.1) - In Development
- Fix issue with alert-level filters where notification would not be sent if first detected label was filtered

## [v0.4.0](https://github.com/0x2142/frigate-notify/releases/tag/v0.4.0) - Jan 27 2025
- Support for notification based on Alerts & Detections via Frigate Reviews
- ⚠️ **Note:** Reviews mode is now the default with this release
Expand Down
2 changes: 1 addition & 1 deletion events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func processEvent(event models.Event) {
}

// Send alert with snapshot
notifier.SendAlert(event)
notifier.SendAlert([]models.Event{event})
}

func recheckEvent(event models.Event) models.Event {
Expand Down
17 changes: 7 additions & 10 deletions events/reviews.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func processReview(review models.Review) {
audioEvent.Extra.Audio = strings.Join(review.Data.Audio, ",")
audioEvent.Camera = review.Camera
audioEvent.Extra.ReviewLink = config.ConfigData.Frigate.PublicURL + "/review?id=" + review.ID
notifier.SendAlert(audioEvent)
notifier.SendAlert([]models.Event{audioEvent})
return
} else {
log.Info().
Expand All @@ -64,7 +64,7 @@ func processReview(review models.Review) {

// Retrieve detailed detection information
reviewFiltered := false
var firstDetection models.Event
var detections []models.Event
for _, id := range review.Data.Detections {
url := fmt.Sprintf("%s/api/events/%s", config.ConfigData.Frigate.Server, id)

Expand Down Expand Up @@ -96,10 +96,10 @@ func processReview(review models.Review) {
break
}

// Store first detection for this review, alerts will be based on this event's data
if firstDetection.ID == "" {
firstDetection = detection
}
// Add special link to review page
detection.Extra.ReviewLink = config.ConfigData.Frigate.PublicURL + "/review?id=" + review.ID

detections = append(detections, detection)
}
// If any detection would be filtered, skip notifying on this review
if reviewFiltered {
Expand All @@ -109,11 +109,8 @@ func processReview(review models.Review) {
return
}

// Add special link to review page
firstDetection.Extra.ReviewLink = config.ConfigData.Frigate.PublicURL + "/review?id=" + review.ID

// Send alert with snapshot
notifier.SendAlert(firstDetection)
notifier.SendAlert(detections)
}

func recheckReview(review models.Review) models.Review {
Expand Down
1 change: 1 addition & 0 deletions models/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type Event struct {
type ExtraFields struct {
FormattedTime string
TopScorePercent string
LabelList string
ZoneList string
LocalURL string
PublicURL string
Expand Down
81 changes: 49 additions & 32 deletions notifier/alerts.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,20 @@ type notifMeta struct {
}

// SendAlert forwards alert information to all enabled alerting methods
func SendAlert(event models.Event) {
func SendAlert(events []models.Event) {
config.Internal.Status.LastNotification = time.Now()

// Collect snapshot, if available
var snapshot io.Reader
if event.HasSnapshot {
snapshot = GetSnapshot(event.ID)
for _, event := range events {
if event.HasSnapshot {
snapshot = GetSnapshot(event.ID)
break
}
}

// Set Event link
event.Extra.EventLink = config.ConfigData.Frigate.PublicURL + "/api/events/" + event.ID + "/clip.mp4"

// Add Frigate Major version metadata
event.Extra.FrigateMajorVersion = config.Internal.FrigateVersion
// Set extra event details & get event used for notifications
event := setExtras(events)

// Create copy of snapshot for each alerting method
var snap []byte
Expand All @@ -54,7 +55,7 @@ func SendAlert(event models.Event) {
for id, profile := range config.ConfigData.Alerts.Discord {
if profile.Enabled {
provider := notifMeta{name: "discord", index: id}
if checkAlertFilters(event, profile.Filters, provider) {
if checkAlertFilters(events, profile.Filters, provider) {
go SendDiscordMessage(event, bytes.NewReader(snap), provider)
}
}
Expand All @@ -63,7 +64,7 @@ func SendAlert(event models.Event) {
for id, profile := range config.ConfigData.Alerts.Gotify {
if profile.Enabled {
provider := notifMeta{name: "gotify", index: id}
if checkAlertFilters(event, profile.Filters, provider) {
if checkAlertFilters(events, profile.Filters, provider) {
go SendGotifyPush(event, provider)
}
}
Expand All @@ -72,7 +73,7 @@ func SendAlert(event models.Event) {
for id, profile := range config.ConfigData.Alerts.SMTP {
if profile.Enabled {
provider := notifMeta{name: "smtp", index: id}
if checkAlertFilters(event, profile.Filters, provider) {
if checkAlertFilters(events, profile.Filters, provider) {
go SendSMTP(event, bytes.NewReader(snap), provider)
}
}
Expand All @@ -81,7 +82,7 @@ func SendAlert(event models.Event) {
for id, profile := range config.ConfigData.Alerts.Telegram {
if profile.Enabled {
provider := notifMeta{name: "telegram", index: id}
if checkAlertFilters(event, profile.Filters, provider) {
if checkAlertFilters(events, profile.Filters, provider) {
go SendTelegramMessage(event, bytes.NewReader(snap), provider)
}
}
Expand All @@ -90,7 +91,7 @@ func SendAlert(event models.Event) {
for id, profile := range config.ConfigData.Alerts.Pushover {
if profile.Enabled {
provider := notifMeta{name: "pushover", index: id}
if checkAlertFilters(event, profile.Filters, provider) {
if checkAlertFilters(events, profile.Filters, provider) {
go SendPushoverMessage(event, bytes.NewReader(snap), provider)
}
}
Expand All @@ -99,7 +100,7 @@ func SendAlert(event models.Event) {
for id, profile := range config.ConfigData.Alerts.Ntfy {
if profile.Enabled {
provider := notifMeta{name: "ntfy", index: id}
if checkAlertFilters(event, profile.Filters, provider) {
if checkAlertFilters(events, profile.Filters, provider) {
go SendNtfyPush(event, bytes.NewReader(snap), provider)
}
}
Expand All @@ -108,7 +109,7 @@ func SendAlert(event models.Event) {
for id, profile := range config.ConfigData.Alerts.Webhook {
if profile.Enabled {
provider := notifMeta{name: "webhook", index: id}
if checkAlertFilters(event, profile.Filters, provider) {
if checkAlertFilters(events, profile.Filters, provider) {
go SendWebhook(event, provider)
}
}
Expand Down Expand Up @@ -143,41 +144,59 @@ func GetSnapshot(eventID string) io.Reader {
}

// setExtras adds additional data into the event model to be used for templates
func setExtras(event models.Event) models.Event {
func setExtras(events []models.Event) models.Event {
// Pull first event, which will be used to store info relevant to notifications
key := events[0]

// Set Event link
key.Extra.EventLink = config.ConfigData.Frigate.PublicURL + "/api/events/" + key.ID + "/clip.mp4"

// Add Frigate Major version metadata
key.Extra.FrigateMajorVersion = config.Internal.FrigateVersion

// Transform camera names, example: "test_camera" to "Test Camera"
caser := cases.Title(language.Und)
event.Extra.CameraName = caser.String(strings.ReplaceAll(event.Camera, "_", " "))
key.Extra.CameraName = caser.String(strings.ReplaceAll(key.Camera, "_", " "))

// Assign Frigate URL to extra event fields
event.Extra.LocalURL = config.ConfigData.Frigate.Server
event.Extra.PublicURL = config.ConfigData.Frigate.PublicURL
key.Extra.LocalURL = config.ConfigData.Frigate.Server
key.Extra.PublicURL = config.ConfigData.Frigate.PublicURL

// Create list of all detected object (mostly applicable to /reviews)
var labelList []string
for _, event := range events {
if !slices.Contains(labelList, event.Label) {
labelList = append(labelList, event.Label)
}
}
key.Extra.LabelList = strings.Join(labelList, ", ")

// MQTT uses CurrentZones, Web API uses Zones
// Combine into one object to use regardless of connection method
event.Zones = append(event.Zones, event.CurrentZones...)
for _, event := range events {
key.Zones = append(key.Zones, event.CurrentZones...)
}
// Remove duplicates
slices.Sort(event.Zones)
event.Zones = slices.Compact(event.Zones)
slices.Sort(key.Zones)
key.Zones = slices.Compact(key.Zones)
// Join zones into plain comma-separated string
event.Extra.ZoneList = strings.Join(event.Zones, ", ")
key.Extra.ZoneList = strings.Join(key.Zones, ", ")

// If certain time format is provided, re-format date / time string
eventTime := time.Unix(int64(event.StartTime), 0)
event.Extra.FormattedTime = eventTime.String()
eventTime := time.Unix(int64(key.StartTime), 0)
key.Extra.FormattedTime = eventTime.String()
if config.ConfigData.Alerts.General.TimeFormat != "" {
event.Extra.FormattedTime = eventTime.Format(config.ConfigData.Alerts.General.TimeFormat)
key.Extra.FormattedTime = eventTime.Format(config.ConfigData.Alerts.General.TimeFormat)
}

// Calc TopScore percentage
event.Extra.TopScorePercent = fmt.Sprintf("%v%%", int((event.TopScore * 100)))
key.Extra.TopScorePercent = fmt.Sprintf("%v%%", int((key.TopScore * 100)))

return event
return key
}

// Build notification based on template
func renderMessage(sourceTemplate string, event models.Event, mtype string, provider string) string {
event = setExtras(event)

// Render template
var tmpl *template.Template
var err error
Expand Down Expand Up @@ -210,8 +229,6 @@ func renderMessage(sourceTemplate string, event models.Event, mtype string, prov

// Build HTTP headers or params based on template
func renderHTTPKV(list []map[string]string, event models.Event, kvtype string, provider string) []map[string]string {
event = setExtras(event)

var renderedList []map[string]string

for _, item := range list {
Expand Down
Loading

0 comments on commit e03865e

Please sign in to comment.