Rewrite camera uptime monitor in Golang
This commit is contained in:
parent
fd4c30f655
commit
90efd78669
|
@ -119,7 +119,7 @@
|
||||||
register: docker_build_dir
|
register: docker_build_dir
|
||||||
- name: Copy Docker build directory
|
- name: Copy Docker build directory
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
src: uptime/
|
src: uptime-go/
|
||||||
dest: '{{docker_build_dir.path}}'
|
dest: '{{docker_build_dir.path}}'
|
||||||
mode: preserve
|
mode: preserve
|
||||||
- name: Build frigate-uptime Docker image
|
- name: Build frigate-uptime Docker image
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
#FROM golang:1.23
|
||||||
|
FROM golang:1.23
|
||||||
|
WORKDIR /code
|
||||||
|
|
||||||
|
ENTRYPOINT ["./uptime"]
|
||||||
|
ENV GOEXPERIMENT=loopvar
|
||||||
|
|
||||||
|
RUN apt update --fix-missing
|
||||||
|
RUN apt install -y iputils-ping ffmpeg
|
||||||
|
|
||||||
|
COPY uptime .
|
||||||
|
RUN go build .
|
|
@ -0,0 +1,26 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
import "gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Cameras map[string]string `yaml:"cameras"`
|
||||||
|
PingIntervalSeconds int64 `yaml:"ping_interval_s"`
|
||||||
|
PingTimeoutSeconds int64 `yaml:"ping_timeout_s"`
|
||||||
|
ConsecutiveDownThreshold int `yaml:"consecutive_down_threshold"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadConfig(configFilename string) Config {
|
||||||
|
var data, readErr = os.ReadFile(configFilename)
|
||||||
|
if readErr != nil {
|
||||||
|
panic(readErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
if yamlErr := yaml.Unmarshal(data, &config); yamlErr != nil {
|
||||||
|
panic(yamlErr)
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
cameras:
|
||||||
|
back_door: 'rtsp://frigate:8554/back_door'
|
||||||
|
doorbell: 'rtsp://frigate:8554/doorbell'
|
||||||
|
front_door: 'rtsp://frigate:8554/front_door'
|
||||||
|
garage: 'rtsp://frigate:8554/garage'
|
||||||
|
ping_interval_s: 60
|
||||||
|
ping_timeout_s: 30
|
||||||
|
consecutive_down_threshold: 3
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
module frigate/uptime
|
||||||
|
|
||||||
|
go 1.21.12
|
||||||
|
|
||||||
|
require gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
@ -0,0 +1,3 @@
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -0,0 +1,17 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"frigate/uptime/config"
|
||||||
|
"frigate/uptime/notify"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var uptimeConfig = config.ReadConfig("config/config.yaml")
|
||||||
|
var pingInterval = time.Duration(uptimeConfig.PingIntervalSeconds * int64(time.Second))
|
||||||
|
var pingTimeout = time.Duration(uptimeConfig.PingTimeoutSeconds * int64(time.Second))
|
||||||
|
|
||||||
|
var cameraMonitor CameraMonitor = NewCameraMonitor(pingInterval, pingTimeout, uptimeConfig.ConsecutiveDownThreshold, notify.NewNtfyNotifier())
|
||||||
|
cameraMonitor.Run(uptimeConfig.Cameras)
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
import (
|
||||||
|
"frigate/uptime/ping"
|
||||||
|
"frigate/uptime/notify"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
type CameraMonitor struct {
|
||||||
|
pingInterval time.Duration
|
||||||
|
pingTimeout time.Duration
|
||||||
|
consecutiveDownThreshold int
|
||||||
|
downtime map[string]int
|
||||||
|
notify.Notifier
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCameraMonitor(pingInterval time.Duration, pingTimeout time.Duration, consecutiveDownThreshold int, notifier notify.Notifier) CameraMonitor {
|
||||||
|
return CameraMonitor{
|
||||||
|
pingInterval: pingInterval,
|
||||||
|
pingTimeout: pingTimeout,
|
||||||
|
consecutiveDownThreshold: consecutiveDownThreshold,
|
||||||
|
downtime: make(map[string]int),
|
||||||
|
Notifier: notifier,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CameraMonitor) onCameraUp(camera string) {
|
||||||
|
c.SendNotification(camera, true)
|
||||||
|
slog.Info(camera, "camera is back online!")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CameraMonitor) onCameraDown(camera string) {
|
||||||
|
c.SendNotification(camera, false)
|
||||||
|
slog.Info(camera, "camera is offline!")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CameraMonitor) onCameraPingResult(camera string, online bool) {
|
||||||
|
if online {
|
||||||
|
if c.downtime[camera] >= c.consecutiveDownThreshold {
|
||||||
|
c.onCameraUp(camera)
|
||||||
|
}
|
||||||
|
c.downtime[camera] = 0
|
||||||
|
} else {
|
||||||
|
c.downtime[camera] += 1
|
||||||
|
if c.downtime[camera] == c.consecutiveDownThreshold {
|
||||||
|
c.onCameraDown(camera)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CameraMonitor) Run(cameras map[string]string) {
|
||||||
|
type pingResult struct {
|
||||||
|
camera string
|
||||||
|
online bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var pingResultChannel = make(chan pingResult, 4)
|
||||||
|
|
||||||
|
for {
|
||||||
|
var unknownPingResultCameras = make(map[string]bool, len(cameras))
|
||||||
|
var startTime = time.Now()
|
||||||
|
var timeoutChannel = time.After(c.pingTimeout)
|
||||||
|
|
||||||
|
// Start all pings
|
||||||
|
for camera, host := range cameras {
|
||||||
|
go func() {
|
||||||
|
pingResultChannel <- pingResult{
|
||||||
|
camera: camera,
|
||||||
|
online: ping.VideoPing(host),
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
unknownPingResultCameras[camera] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout:
|
||||||
|
// Await ping results or timeout
|
||||||
|
for range cameras {
|
||||||
|
select {
|
||||||
|
case cameraPingResult := <-pingResultChannel:
|
||||||
|
slog.Debug(cameraPingResult.camera, "camera:", cameraPingResult.online)
|
||||||
|
c.onCameraPingResult(cameraPingResult.camera, cameraPingResult.online)
|
||||||
|
delete(unknownPingResultCameras, cameraPingResult.camera) // Maintain set of cameras with unknown ping status
|
||||||
|
case <-timeoutChannel:
|
||||||
|
slog.Info("Timed out waiting for cameras", unknownPingResultCameras)
|
||||||
|
break timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle timed out camera pings
|
||||||
|
for camera := range unknownPingResultCameras {
|
||||||
|
c.onCameraPingResult(camera, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sleepDuration = c.pingInterval - time.Since(startTime)
|
||||||
|
slog.Debug("Sleeping for", sleepDuration)
|
||||||
|
time.Sleep(sleepDuration)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package notify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
type Notifier interface {
|
||||||
|
SendNotification(camera string, online bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMessage(camera string, online bool) string {
|
||||||
|
var msgTemplates = map[bool]string{
|
||||||
|
true: "%s camera is back online!",
|
||||||
|
false: "%s camera is offline!",
|
||||||
|
}
|
||||||
|
|
||||||
|
var cameraTitle string
|
||||||
|
for i, s := range strings.Split(camera, "_") {
|
||||||
|
if i > 0 {
|
||||||
|
cameraTitle += " "
|
||||||
|
}
|
||||||
|
cameraTitle += strings.ToUpper(string(s[0])) + s[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(msgTemplates[online], cameraTitle)
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package notify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
type NtfyNotifier struct {
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNtfyNotifier() NtfyNotifier {
|
||||||
|
return NtfyNotifier{
|
||||||
|
client: &http.Client{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n NtfyNotifier) SendNotification(camera string, online bool) {
|
||||||
|
type ntfyJson struct {
|
||||||
|
Topic string `json:"topic"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Priority int `json:"priority"`
|
||||||
|
Click string `json:"click"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var reqJson, jsonErr = json.Marshal(ntfyJson{
|
||||||
|
Topic: "frigate_camera_uptime",
|
||||||
|
Title: "Frigate",
|
||||||
|
Message: createMessage(camera, online),
|
||||||
|
Priority: 3,
|
||||||
|
Click: fmt.Sprintf("https://frigate.homelab.net/cameras/%s", camera),
|
||||||
|
Icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/frigate.png",
|
||||||
|
})
|
||||||
|
if jsonErr != nil {
|
||||||
|
slog.Error("Failed to construct JSON request body", jsonErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req, reqErr = http.NewRequest("POST", "https://ntfy.homelab.net", bytes.NewBuffer([]byte(reqJson)))
|
||||||
|
if reqErr != nil {
|
||||||
|
slog.Error("Failed to create HTTP request", reqErr)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
var resp, respErr = n.client.Do(req)
|
||||||
|
if respErr != nil {
|
||||||
|
slog.Error("Failed to send HTTP request", respErr)
|
||||||
|
}
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
slog.Error("Ntfy notification returned HTTP", resp.Status)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package ping
|
||||||
|
|
||||||
|
import "os/exec"
|
||||||
|
|
||||||
|
|
||||||
|
func IPPing(host string) bool {
|
||||||
|
var cmd = exec.Command("ping", "-w", "3", "-c", "1", host)
|
||||||
|
var err = cmd.Run()
|
||||||
|
return err == nil
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package ping
|
|
@ -0,0 +1,10 @@
|
||||||
|
package ping
|
||||||
|
|
||||||
|
import "os/exec"
|
||||||
|
|
||||||
|
|
||||||
|
func VideoPing(url string) bool {
|
||||||
|
var cmd = exec.Command("ffprobe", "-rtsp_transport", "tcp", "-i", url, "-timeout", "10000000")
|
||||||
|
var err = cmd.Run()
|
||||||
|
return err == nil
|
||||||
|
}
|
Loading…
Reference in New Issue