Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Notif templates #59

Merged
merged 5 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ type Config struct {
}

type Frigate struct {
Server string `fig:"server" validate:"required"`
Insecure bool `fig:"ignoressl" default:false`
WebAPI WebAPI `fig:"webapi"`
MQTT MQTT `fig:"mqtt"`
Cameras Cameras `fig:"cameras"`
Server string `fig:"server" validate:"required"`
Insecure bool `fig:"ignoressl" default:false`
Headers []map[string]string `fig:"headers"`
WebAPI WebAPI `fig:"webapi"`
MQTT MQTT `fig:"mqtt"`
Cameras Cameras `fig:"cameras"`
}

type WebAPI struct {
Expand Down Expand Up @@ -53,6 +54,7 @@ type Alerts struct {
SMTP SMTP `fig:"smtp"`
Telegram Telegram `fig:"telegram"`
Pushover Pushover `fig:"pushover"`
Nfty Nfty `fig:"nfty"`
}

type General struct {
Expand Down Expand Up @@ -110,6 +112,13 @@ type Pushover struct {
TTL int `fig:"ttl" default:0`
}

type Nfty struct {
Enabled bool `fig:"enabled" default:false`
Server string `fig:"server" default:""`
Topic string `fig:"topic" default:""`
Insecure bool `fig:"ignoressl" default:false`
}

type Monitor struct {
Enabled bool `fig:"enabled" default:false`
URL string `fig:"url" default:""`
Expand Down Expand Up @@ -301,6 +310,16 @@ func validateConfig() {
configErrors = append(configErrors, "Pushover TTL cannot be negative!")
}
}
if ConfigData.Alerts.Nfty.Enabled {
log.Print("Nfty alerting enabled.")
if ConfigData.Alerts.Nfty.Server == "" {
configErrors = append(configErrors, "No Nfty server specified!")
}
if ConfigData.Alerts.Nfty.Topic == "" {
configErrors = append(configErrors, "No Nfty topic specified!")
}

}

// Validate monitoring config
if ConfigData.Monitor.Enabled {
Expand Down
6 changes: 6 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [v0.2.8](https://github.com/0x2142/frigate-notify/releases/tag/v0.2.8) - Upcoming Release

- Add support for notifications via [Nfty](https://frigate-notify.0x2142.com/config/#nfty)
- Add ability to send additional HTTP [headers](https://frigate-notify.0x2142.com/config/#frigate) to Frigate
- Rework event notifications to be built from templates

## [v0.2.7](https://github.com/0x2142/frigate-notify/releases/tag/v0.2.7) - May 06 2024

- Allow changing default MQTT topic prefix via config
Expand Down
40 changes: 37 additions & 3 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@ Configuration snippets will be provided throughout this page. Feel free to copy
- IP or hostname of the Frigate NVR
- **ignoressl** (Optional - Default: `false`)
- Set to `true` to allow self-signed certificates
- **headers** (Optional)
- Send additional HTTP headers to Frigate
- Useful for things like authentication
- Header format: `Header: Value`
- Example: `Authorization: Basic abcd1234`

```yaml title="Config File Snippet"
frigate:
server: nvr.your.domain.tld
ignoressl: true
headers:
- Authorization: Basic abcd1234
```

### WebAPI
Expand Down Expand Up @@ -324,6 +331,28 @@ alerts:
ttl:
```

### Nfty

- **enabled** (Optional - Default: `false`)
- Set to `true` to enable alerting via Nfty
- **server** (Required)
- Full URL of the desired Nfty server
- Required if this alerting method is enabled
- **topic** (Required)
- Destination topic that will receive alert notifications
- Required if this alerting method is enabled
- **ignoressl** (Optional - Default: `false`)
- Set to `true` to allow self-signed certificates

```yaml title="Config File Snippet"
alerts:
nfty:
enabled: true
server: https://nfty.your.domain.tld
topic: frigate
ignoressl: true
```

## Monitor

If enabled, this application will check in with tools like [HealthChecks](https://github.com/healthchecks/healthchecks) or [Uptime Kuma](https://github.com/louislam/uptime-kuma) on a regular interval for health / status monitoring.
Expand All @@ -347,10 +376,8 @@ monitor:
ignoressl:
```


---


## Sample Config { data-search-exclude }

A full config file template has been provided below:
Expand All @@ -359,6 +386,7 @@ A full config file template has been provided below:
frigate:
server:
ignoressl:
headers:

webapi:
enabled:
Expand Down Expand Up @@ -431,9 +459,15 @@ alerts:
expire:
ttl:

nfty:
enabled: false
server:
topic:
ignoressl:

monitor:
enabled: false
url:
interval:
ignoressl:
```
```
12 changes: 6 additions & 6 deletions events/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"time"

"github.com/0x2142/frigate-notify/config"
"github.com/0x2142/frigate-notify/models"
"github.com/0x2142/frigate-notify/notifier"
"github.com/0x2142/frigate-notify/util"
"golang.org/x/exp/slices"
Expand All @@ -35,12 +36,13 @@ func CheckForEvents() {
log.Println("Checking for new events...")

// Query events
response, err := util.HTTPGet(url, config.ConfigData.Frigate.Insecure)
response, err := util.HTTPGet(url, config.ConfigData.Frigate.Insecure, config.ConfigData.Frigate.Headers...)
if err != nil {
log.Printf("Cannot get events from %s", url)
log.Printf("Error received: %s", err)
}

var events []Event
var events []models.Event

json.Unmarshal([]byte(response), &events)

Expand Down Expand Up @@ -77,17 +79,15 @@ func CheckForEvents() {
snapshot = GetSnapshot(snapshotURL, event.ID)
}

message := buildMessage(eventTime, event)

// Send alert with snapshot
notifier.SendAlert(message, snapshotURL, snapshot, event.ID)
notifier.SendAlert(event, snapshotURL, snapshot, event.ID)
}

}

// GetSnapshot downloads a snapshot from Frigate
func GetSnapshot(snapshotURL, eventID string) io.Reader {
response, err := util.HTTPGet(snapshotURL, config.ConfigData.Frigate.Insecure)
response, err := util.HTTPGet(snapshotURL, config.ConfigData.Frigate.Insecure, config.ConfigData.Frigate.Headers...)
if err != nil {
log.Println("Could not access snaphot. Error: ", err)
}
Expand Down
57 changes: 0 additions & 57 deletions events/events.go
Original file line number Diff line number Diff line change
@@ -1,70 +1,13 @@
package frigate

import (
"fmt"
"log"
"slices"
"strings"
"time"

"github.com/0x2142/frigate-notify/config"
)

// Event stores Frigate alert attributes
type Event struct {
Area interface{} `json:"area"`
Box interface{} `json:"box"`
Camera string `json:"camera"`
EndTime interface{} `json:"end_time"`
FalsePositive interface{} `json:"false_positive"`
HasClip bool `json:"has_clip"`
HasSnapshot bool `json:"has_snapshot"`
ID string `json:"id"`
Label string `json:"label"`
PlusID interface{} `json:"plus_id"`
Ratio interface{} `json:"ratio"`
Region interface{} `json:"region"`
RetainIndefinitely bool `json:"retain_indefinitely"`
StartTime float64 `json:"start_time"`
SubLabel interface{} `json:"sub_label"`
Thumbnail string `json:"thumbnail"`
TopScore float64 `json:"top_score"`
Zones []string `json:"zones"`
CurrentZones []string `json:"current_zones"`
EnteredZones []string `json:"entered_zones"`
}

// buildMessage constructs message payload for all alerting methods
func buildMessage(time time.Time, event Event) string {
// If certain time format is provided, re-format date / time string
timestr := time.String()
if config.ConfigData.Alerts.General.TimeFormat != "" {
timestr = time.Format(config.ConfigData.Alerts.General.TimeFormat)
}
// Build alert message payload, include two spaces at end to force markdown newline
message := fmt.Sprintf("Detection at %v ", timestr)
message += fmt.Sprintf("\nCamera: %s ", event.Camera)
// Attach detection label & caculate score percentage
message += fmt.Sprintf("\nLabel: %v (%v%%) ", event.Label, int((event.TopScore * 100)))
// If zones configured / detected, include details
var zones []string
zones = append(zones, event.Zones...)
zones = append(zones, event.CurrentZones...)
if len(zones) >= 1 {
message += fmt.Sprintf("\nZone(s): %v ", strings.Join(zones, ", "))
}
// Append link to camera
message += "\n\nLinks: "
message += fmt.Sprintf("[Camera](%s/cameras/%s)", config.ConfigData.Frigate.Server, event.Camera)
// If event has a recorded clip, include a link to that as well
if event.HasClip {
message += " | "
message += fmt.Sprintf("[Event Clip](%s/api/events/%s/clip.mp4) ", config.ConfigData.Frigate.Server, event.ID)
}

return message
}

// isAllowedZone verifies whether a zone should be allowed to generate a notification
func isAllowedZone(id string, zones []string) bool {
// By default, send events without a zone unless specified otherwise
Expand Down
18 changes: 3 additions & 15 deletions events/mqtt.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,12 @@ import (
"time"

"github.com/0x2142/frigate-notify/config"
"github.com/0x2142/frigate-notify/models"
"github.com/0x2142/frigate-notify/notifier"
mqtt "github.com/eclipse/paho.mqtt.golang"
"golang.org/x/exp/slices"
)

// MQTTEvent stores incoming MQTT payloads from Frigate
type MQTTEvent struct {
Before struct {
Event
} `json:"before,omitempty"`
After struct {
Event
} `json:"after,omitempty"`
Type string `json:"type"`
}

// SubscribeMQTT establishes subscription to MQTT server & listens for messages
func SubscribeMQTT() {
// MQTT client configuration
Expand Down Expand Up @@ -61,7 +51,7 @@ func SubscribeMQTT() {
// processEvent handles incoming MQTT messages & pulls out relevant info for alerting
func processEvent(client mqtt.Client, msg mqtt.Message) {
// Parse incoming MQTT message
var event MQTTEvent
var event models.MQTTEvent
json.Unmarshal(msg.Payload(), &event)

if event.Type == "new" || event.Type == "update" {
Expand Down Expand Up @@ -109,10 +99,8 @@ func processEvent(client mqtt.Client, msg mqtt.Message) {
snapshot = GetSnapshot(snapshotURL, event.After.ID)
}

message := buildMessage(eventTime, event.After.Event)

// Send alert with snapshot
notifier.SendAlert(message, snapshotURL, snapshot, event.After.ID)
notifier.SendAlert(event.After.Event, snapshotURL, snapshot, event.After.ID)
}
}

Expand Down
15 changes: 15 additions & 0 deletions example-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ frigate:
# Set to true if using SSL & a self-signed certificate
ignoressl: false

# List of HTTP headers to send to Frigate, in format Header: Value
headers:
# Example:
# - Authorization: Basic abcd1234

webapi:
# Set to true to enable event collection via the web API
enabled:
Expand Down Expand Up @@ -127,6 +132,16 @@ alerts:
# Optional message lifetime
ttl:

# Nfty Config
nfty:
# Set to true to enable alerting via
enabled: false
# URL of Nfty server
server:
# Nfty topic for notifications
topic:
# Set to true if using SSL & a self-signed certificate
ignoressl:

## App Monitoring
# Sends HTTP GET to provided URL for aliveness checks
Expand Down
44 changes: 44 additions & 0 deletions models/event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package models

// MQTTEvent stores incoming MQTT payloads from Frigate
type MQTTEvent struct {
Before struct {
Event
} `json:"before,omitempty"`
After struct {
Event
} `json:"after,omitempty"`
Type string `json:"type"`
}

// Event stores Frigate alert attributes
type Event struct {
Area interface{} `json:"area"`
Box interface{} `json:"box"`
Camera string `json:"camera"`
EndTime interface{} `json:"end_time"`
FalsePositive interface{} `json:"false_positive"`
HasClip bool `json:"has_clip"`
HasSnapshot bool `json:"has_snapshot"`
ID string `json:"id"`
Label string `json:"label"`
PlusID interface{} `json:"plus_id"`
Ratio interface{} `json:"ratio"`
Region interface{} `json:"region"`
RetainIndefinitely bool `json:"retain_indefinitely"`
StartTime float64 `json:"start_time"`
SubLabel interface{} `json:"sub_label"`
Thumbnail string `json:"thumbnail"`
TopScore float64 `json:"top_score"`
Zones []string `json:"zones"`
CurrentZones []string `json:"current_zones"`
EnteredZones []string `json:"entered_zones"`
Extra ExtraFields
}

// Additional custom fields
type ExtraFields struct {
FormattedTime string
TopScorePercent string
LocalURL string
}
Loading
Loading