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

Minor fixes #60

Merged
merged 14 commits into from
May 15, 2024
Merged
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ FROM scratch
WORKDIR /app

COPY --from=build /frigate-notify /app/frigate-notify
COPY /templates /app/templates
COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

Expand Down
62 changes: 55 additions & 7 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package config

import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"strings"

"github.com/0x2142/frigate-notify/models"
"github.com/0x2142/frigate-notify/util"
"github.com/kkyr/fig"
)

Expand All @@ -17,11 +20,13 @@ 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`
PublicURL string `fig:"public_url" default:""`
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 +58,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 +116,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 @@ -137,11 +150,10 @@ func LoadConfig(configFile string) {
if err != nil {
log.Fatal("Failed to load config file! Error: ", err)
}
log.Print("Config file loaded.")

// Send config file to validation before completing
validateConfig()

log.Print("Config file loaded.")
}

// validateConfig checks config file structure & loads info into associated packages
Expand All @@ -153,6 +165,11 @@ func validateConfig() {
configErrors = append(configErrors, "Please configure only one polling method: Frigate Web API or MQTT")
}

// Set default web API interval if not specified
if ConfigData.Frigate.WebAPI.Enabled && ConfigData.Frigate.WebAPI.Interval == 0 {
ConfigData.Frigate.WebAPI.Interval = 30
}

// Warn on test mode being enabled
if ConfigData.Frigate.WebAPI.Enabled && ConfigData.Frigate.WebAPI.TestMode {
log.Print("~~~~~~~~~~~~~~~~~~~")
Expand All @@ -168,6 +185,27 @@ func validateConfig() {
ConfigData.Frigate.Server = fmt.Sprintf("http://%s", ConfigData.Frigate.Server)
}

// Test connectivity to Frigate
log.Print("Checking connection to Frigate server...")
statsAPI := fmt.Sprintf("%s/api/stats", ConfigData.Frigate.Server)
response, err := util.HTTPGet(statsAPI, ConfigData.Frigate.Insecure)
if err != nil {
log.Fatalf("Cannot reach Frigate server at %v, error: %v", ConfigData.Frigate.Server, err)
}
var stats models.FrigateStats
json.Unmarshal([]byte(response), &stats)
log.Printf("Successfully connected to %v", ConfigData.Frigate.Server)
if stats.Service.Version != "" {
log.Printf("Frigate server is running version %v", stats.Service.Version)
}

// Check Public / External URL if set
if ConfigData.Frigate.PublicURL != "" {
if !strings.Contains(ConfigData.Frigate.PublicURL, "http://") && !strings.Contains(ConfigData.Frigate.PublicURL, "https://") {
configErrors = append(configErrors, "Public URL must include http:// or https://")
}
}

// Check for camera exclusions
if len(ConfigData.Frigate.Cameras.Exclude) > 0 {
log.Println("Cameras to exclude from alerting:")
Expand Down Expand Up @@ -301,6 +339,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
11 changes: 11 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

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

- 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
- Add new `public_url` config item for Frigate
- This will be used in notification links & should be configured if Frigate is accessible via the internet
- Add startup check to verify Frigate API is accessible
- Rework event notifications to be built from templates
- Fix default interval for querying evens via web API
- Fix issue where label score is 0% via web API event query

## [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
56 changes: 49 additions & 7 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,28 @@ Configuration snippets will be provided throughout this page. Feel free to copy
### Server

- **server** (Required)
- IP or hostname of the Frigate NVR
- IP, hostname, or URL of the Frigate NVR
- If IP or hostname specified, app will prepend `http://`
- If Frigate is not behind a reverse proxy, append port number if necessary
- **ignoressl** (Optional - Default: `false`)
- Set to `true` to allow self-signed certificates
- Set to `true` to allow self-signed certificates for `server`
- **public_url** (Optional)
- Should be set if Frigate is available via an external, public URL
- This value is used for the links used in notifications
- Format should be full URL (example: `https://nvr.your.public.domain.tld`)
- **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
public_url: https://nvr.your.public.domain.tld
headers:
- Authorization: Basic abcd1234
```

### WebAPI
Expand All @@ -27,7 +41,7 @@ frigate:
- **enabled** (Optional - Default: `false`)
- If set to `true`, Frigate events are collected by polling the web API
- **interval** (Optional - Default: `30`)
- How frequently to check the Frigate web API for new events, in seconds
- How frequently to check the Frigate web API for new events, in seconds

```yaml title="Config File Snippet"
frigate:
Expand Down Expand Up @@ -324,6 +338,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,18 +383,18 @@ monitor:
ignoressl:
```


---


## Sample Config { data-search-exclude }

A full config file template has been provided below:

```yaml
frigate:
server:
ignoressl:
ignoressl:
public_url:
headers:

webapi:
enabled:
Expand Down Expand Up @@ -431,9 +467,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
Loading
Loading