Setup repeat camera down notifications #24

This commit is contained in:
Ashish D'Souza 2025-05-14 21:15:45 -05:00
parent 7b24dad5b9
commit e6ccd8f1d7
8 changed files with 38 additions and 46 deletions

View File

@ -4,12 +4,12 @@ import "os"
import "gopkg.in/yaml.v3" import "gopkg.in/yaml.v3"
type Config struct { type Config struct {
Cameras map[string]string `yaml:"cameras"` Cameras map[string]string `yaml:"cameras"`
PingIntervalSeconds int64 `yaml:"ping_interval_s"` PingIntervalSeconds int64 `yaml:"ping_interval_s"`
PingTimeoutSeconds int64 `yaml:"ping_timeout_s"` PingTimeoutSeconds int64 `yaml:"ping_timeout_s"`
ConsecutiveDownThreshold int `yaml:"consecutive_down_threshold"` ConsecutiveDownThreshold int `yaml:"consecutive_down_threshold"`
NotificationRepeatThreshold int `yaml:"notification_repeat_threshold"`
} }
func ReadConfig(configFilename string) Config { func ReadConfig(configFilename string) Config {

View File

@ -6,4 +6,4 @@ cameras:
ping_interval_s: 60 ping_interval_s: 60
ping_timeout_s: 30 ping_timeout_s: 30
consecutive_down_threshold: 3 consecutive_down_threshold: 3
notification_repeat_threshold: 30

View File

@ -12,6 +12,6 @@ func main() {
var pingInterval = time.Duration(uptimeConfig.PingIntervalSeconds * int64(time.Second)) var pingInterval = time.Duration(uptimeConfig.PingIntervalSeconds * int64(time.Second))
var pingTimeout = time.Duration(uptimeConfig.PingTimeoutSeconds * int64(time.Second)) var pingTimeout = time.Duration(uptimeConfig.PingTimeoutSeconds * int64(time.Second))
var cameraMonitor CameraMonitor = NewCameraMonitor(pingInterval, pingTimeout, uptimeConfig.ConsecutiveDownThreshold, notify.NewNtfyNotifier()) var cameraMonitor CameraMonitor = NewCameraMonitor(pingInterval, pingTimeout, uptimeConfig.ConsecutiveDownThreshold, uptimeConfig.NotificationRepeatThreshold, notify.NewNtfyNotifier())
cameraMonitor.Run(uptimeConfig.Cameras) cameraMonitor.Run(uptimeConfig.Cameras)
} }

View File

@ -13,44 +13,40 @@ import (
) )
type CameraMonitor struct { type CameraMonitor struct {
pingInterval time.Duration pingInterval time.Duration
pingTimeout time.Duration pingTimeout time.Duration
consecutiveDownThreshold int consecutiveDownThreshold int
downtime map[string]int notificationRepeatThreshold int
downtime map[string]int
notify.Notifier notify.Notifier
} }
func NewCameraMonitor(pingInterval time.Duration, pingTimeout time.Duration, consecutiveDownThreshold int, notifier notify.Notifier) CameraMonitor { func NewCameraMonitor(pingInterval time.Duration, pingTimeout time.Duration, consecutiveDownThreshold int, notificationRepeatThreshold int, notifier notify.Notifier) CameraMonitor {
return CameraMonitor{ return CameraMonitor{
pingInterval: pingInterval, pingInterval: pingInterval,
pingTimeout: pingTimeout, pingTimeout: pingTimeout,
consecutiveDownThreshold: consecutiveDownThreshold, consecutiveDownThreshold: consecutiveDownThreshold,
downtime: make(map[string]int), notificationRepeatThreshold: notificationRepeatThreshold,
Notifier: notifier, downtime: make(map[string]int),
Notifier: notifier,
} }
} }
func (c CameraMonitor) onCameraUp(camera string) {
c.SendNotification(camera, true)
slog.Info(fmt.Sprintf("%s camera is back online!", camera))
}
func (c CameraMonitor) onCameraDown(camera string) {
c.SendNotification(camera, false)
slog.Info(fmt.Sprintf("%s camera is offline!", camera))
}
func (c CameraMonitor) onCameraPingResult(camera string, online bool) { func (c CameraMonitor) onCameraPingResult(camera string, online bool) {
if online { if online {
if c.downtime[camera] >= c.consecutiveDownThreshold { if c.downtime[camera] >= c.consecutiveDownThreshold {
c.onCameraUp(camera) c.SendNotification(camera, true)
slog.Info(fmt.Sprintf("%s camera is back online!", camera))
} }
c.downtime[camera] = 0 c.downtime[camera] = 0
} else { } else {
c.downtime[camera] += 1 c.downtime[camera] += 1
if c.downtime[camera] == c.consecutiveDownThreshold { if c.downtime[camera] == c.consecutiveDownThreshold {
c.onCameraDown(camera) c.SendNotification(camera, false)
slog.Info(fmt.Sprintf("%s camera is offline!", camera))
} else if c.notificationRepeatThreshold >= c.consecutiveDownThreshold && c.downtime[camera]%c.notificationRepeatThreshold == c.consecutiveDownThreshold {
c.SendNotification(camera, false)
} }
} }
} }

View File

@ -5,14 +5,13 @@ import (
"strings" "strings"
) )
type Notifier interface { type Notifier interface {
SendNotification(camera string, online bool) SendNotification(camera string, online bool)
} }
func createMessage(camera string, online bool) string { func createMessage(camera string, online bool) string {
var msgTemplates = map[bool]string{ var msgTemplates = map[bool]string{
true: "%s camera is back online!", true: "%s camera is back online!",
false: "%s camera is offline!", false: "%s camera is offline!",
} }

View File

@ -1,15 +1,14 @@
package notify package notify
import ( import (
"crypto/tls"
"bytes" "bytes"
"crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log/slog" "log/slog"
"net/http" "net/http"
) )
type NtfyNotifier struct { type NtfyNotifier struct {
client *http.Client client *http.Client
} }
@ -26,21 +25,21 @@ func NewNtfyNotifier() NtfyNotifier {
func (n NtfyNotifier) SendNotification(camera string, online bool) { func (n NtfyNotifier) SendNotification(camera string, online bool) {
type ntfyJson struct { type ntfyJson struct {
Topic string `json:"topic"` Topic string `json:"topic"`
Title string `json:"title"` Title string `json:"title"`
Message string `json:"message"` Message string `json:"message"`
Priority int `json:"priority"` Priority int `json:"priority"`
Click string `json:"click"` Click string `json:"click"`
Icon string `json:"icon"` Icon string `json:"icon"`
} }
var reqJson, jsonErr = json.Marshal(ntfyJson{ var reqJson, jsonErr = json.Marshal(ntfyJson{
Topic: "frigate_camera_uptime", Topic: "frigate_camera_uptime",
Title: "Frigate", Title: "Frigate",
Message: createMessage(camera, online), Message: createMessage(camera, online),
Priority: 3, Priority: 3,
Click: fmt.Sprintf("https://frigate.homelab.net/cameras/%s", camera), Click: fmt.Sprintf("https://frigate.homelab.net/cameras/%s", camera),
Icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/frigate.png", Icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/frigate.png",
}) })
if jsonErr != nil { if jsonErr != nil {
slog.Error("Failed to construct JSON request body", "error", jsonErr) slog.Error("Failed to construct JSON request body", "error", jsonErr)

View File

@ -2,7 +2,6 @@ package ping
import "os/exec" import "os/exec"
func IPPing(host string) bool { func IPPing(host string) bool {
var cmd = exec.Command("ping", "-w", "3", "-c", "1", host) var cmd = exec.Command("ping", "-w", "3", "-c", "1", host)
var err = cmd.Run() var err = cmd.Run()

View File

@ -2,7 +2,6 @@ package ping
import "os/exec" import "os/exec"
func VideoPing(url string) bool { func VideoPing(url string) bool {
var cmd = exec.Command("ffprobe", "-rtsp_transport", "tcp", "-i", url, "-timeout", "10000000") var cmd = exec.Command("ffprobe", "-rtsp_transport", "tcp", "-i", url, "-timeout", "10000000")
var err = cmd.Run() var err = cmd.Run()