Rewrite camera uptime monitor in Golang
This commit is contained in:
parent
fd4c30f655
commit
90efd78669
|
@ -119,7 +119,7 @@
|
|||
register: docker_build_dir
|
||||
- name: Copy Docker build directory
|
||||
ansible.builtin.copy:
|
||||
src: uptime/
|
||||
src: uptime-go/
|
||||
dest: '{{docker_build_dir.path}}'
|
||||
mode: preserve
|
||||
- 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