Skip to content

Commit

Permalink
Merge pull request #195 from 0x2142/dev
Browse files Browse the repository at this point in the history
Release v0.4.0
  • Loading branch information
0x2142 authored Jan 27, 2025
2 parents 8773463 + cef105e commit 42e5cc2
Show file tree
Hide file tree
Showing 63 changed files with 3,585 additions and 997 deletions.
11 changes: 10 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
.vscode
.env
*.tmp
*.bak
# Go build and workspace files
frigate-notify
go.work
go.work.sum
dist/
# Mkdocs
site/
# Local test config
config.yml
*config.yml
Taskfile.yaml
mqttlogger/
log/
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.22-alpine as build
FROM golang:1.22-alpine AS build

WORKDIR /app

Expand All @@ -21,4 +21,6 @@ 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/

EXPOSE 8000

ENTRYPOINT [ "/app/frigate-notify" ]
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Frigate-notify
# Frigate-Notify

<div align="center">

Expand All @@ -8,9 +8,7 @@

## About

This project is designed to generate event notifications from a standalone [Frigate](https://github.com/blakeblackshear/frigate) NVR instance.

Currently Frigate only supports notifications through Home Assistant, which I'm not using right now. So I set out to build a simple notification app that would work with a standalone Frigate server.
Frigate-Notify is a simple app designed to send notifications from [Frigate](https://github.com/blakeblackshear/frigate) NVR to your favorite platforms. Intended to be used with standalone Frigate installations - Home Assistant not required, MQTT is optional but recommended.

## Features

Expand Down
38 changes: 38 additions & 0 deletions api/router.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package api

import (
"fmt"
"net"
"net/http"

apiv1 "github.com/0x2142/frigate-notify/api/v1"
"github.com/0x2142/frigate-notify/config"
"github.com/danielgtaylor/huma/v2"
"github.com/danielgtaylor/huma/v2/adapters/humago"
"github.com/rs/zerolog/log"
)

func RunAPIServer() error {

router := http.NewServeMux()

// Configure API
apiConfig := huma.DefaultConfig("Frigate-Notify", config.Internal.AppVersion)
apiConfig.Info.License = &huma.License{Name: "MIT",
URL: "https://github.com/0x2142/frigate-notify/blob/main/LICENSE"}
apiConfig.Info.Contact = &huma.Contact{Name: "Matt Schmitz",
URL: "https://github.com/0x2142/frigate-notify",
}
api := humago.New(router, apiConfig)

apiv1.Registerv1Routes(api)

log.Debug().Msg("Starting API server...")
listenAddr := fmt.Sprintf("0.0.0.0:%v", config.ConfigData.App.API.Port)
listener, err := net.Listen("tcp", listenAddr)
if err != nil {
return err
}
go http.Serve(listener, router)
return nil
}
113 changes: 113 additions & 0 deletions api/v1/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package apiv1

import (
"context"

"github.com/0x2142/frigate-notify/config"
"github.com/0x2142/frigate-notify/events"
"github.com/danielgtaylor/huma/v2"
"github.com/rs/zerolog/log"
)

type ConfigOutput struct {
Body struct {
Config config.Config `json:"config"`
}
}

type PutConfigInput struct {
Body struct {
Config config.Config `json:"config"`
SkipSave bool `json:"skipsave,omitempty" doc:"Skip writing new config to file" default:"false"`
SkipBackup bool `json:"skipbackup,omitempty" doc:"Skip creating config file backup" default:"false"`
SkipValidate bool `json:"skipvalidate,omitempty" doc:"Skip config validation checking"`
SkipReload bool `json:"skipreload,omitempty" doc:"Skip config reload after updating settings" hidden:"true"`
}
}

type PutConfigOutput struct {
Body struct {
Status string `json:"status"`
Errors []string `json:"errors,omitempty"`
}
}

// GetConfig returns the current running configuration
func GetConfig(ctx context.Context, input *struct{}) (*ConfigOutput, error) {
log.Trace().
Str("uri", V1_PREFIX+"/config").
Str("method", "GET").
Msg("Received API request")

resp := &ConfigOutput{}
resp.Body.Config = config.ConfigData

log.Trace().
Str("uri", V1_PREFIX+"/config").
Interface("response_json", resp.Body).
Msg("Sent API response")

return resp, nil
}

// PutConfig replaces the current running configuration
func PutConfig(ctx context.Context, input *PutConfigInput) (*PutConfigOutput, error) {
log.Trace().
Str("uri", V1_PREFIX+"/config").
Str("method", "PUT").
Msg("Received API request")

resp := &PutConfigOutput{}

newConfig := input.Body.Config

var validationErrors []string
if !input.Body.SkipValidate {
log.Trace().Msg("Skipping config validation checks")
validationErrors = newConfig.Validate()
}

if len(validationErrors) == 0 {
resp.Body.Status = "ok"
if !input.Body.SkipReload {
go reloadCfg(newConfig, input.Body.SkipSave, input.Body.SkipBackup)
}

log.Trace().
Str("uri", V1_PREFIX+"/config").
Interface("response_json", resp.Body).
Msg("Sent API response")
return resp, nil
} else {
resp.Body.Status = "validation error"
resp.Body.Errors = validationErrors

log.Trace().
Str("uri", V1_PREFIX+"/config").
Interface("response_json", resp.Body).
Msg("Sent API response")

return resp, huma.Error422UnprocessableEntity("config validation failed")
}
}

func reloadCfg(newconfig config.Config, skipSave bool, skipBackup bool) {
log.Info().Msg("Reloading app config...")
log.Trace().
Bool("skipSave", skipSave).
Bool("skipBackup", skipBackup).
Msg("Config reload via API")
if config.ConfigData.Frigate.MQTT.Enabled {
events.DisconnectMQTT()
}

config.ConfigData = newconfig
if !skipSave {
config.Save(skipBackup)
}

if config.ConfigData.Frigate.MQTT.Enabled {
events.SubscribeMQTT()
}
log.Info().Msg("Config reload completed")
}
50 changes: 50 additions & 0 deletions api/v1/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package apiv1

import (
"bytes"
"net/http"
"testing"

"github.com/danielgtaylor/huma/v2/humatest"
)

func TestGetConfig(t *testing.T) {
_, api := humatest.New(t)

Registerv1Routes(api)

resp := api.Get("/api/v1/config")

if resp.Code != http.StatusOK {
t.Error("Expected HTTP 200, got ", resp.Code)
}
}

func TestPutConfig(t *testing.T) {
_, api := humatest.New(t)

Registerv1Routes(api)

newconfig := `{
"config":{
"frigate":{
"server":"http://192.0.2.10:5000",
"mqtt":{
"enabled": true
}
},
"alerts":{
}
},
"skipvalidate": true,
"skipbackup": true,
"skipsave": true,
"skipreload": true
}`

resp := api.Put("/api/v1/config", bytes.NewReader([]byte(newconfig)))

if resp.Code != http.StatusAccepted {
t.Error("Expected HTTP 202, got ", resp.Code)
}
}
32 changes: 32 additions & 0 deletions api/v1/healthz.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package apiv1

import (
"context"

"github.com/rs/zerolog/log"
)

type HealthzOutput struct {
Body struct {
Status string `json:"status"`
}
}

// GetHealthz returns current app liveness state
func GetHealthz(ctx context.Context, input *struct{}) (*HealthzOutput, error) {
log.Trace().
Str("uri", V1_PREFIX+"/healthz").
Str("method", "GET").
Msg("Received API request")

resp := &HealthzOutput{}

resp.Body.Status = "ok"

log.Trace().
Str("uri", V1_PREFIX+"/healthz").
Interface("response_json", resp.Body).
Msg("Sent API response")
return resp, nil

}
28 changes: 28 additions & 0 deletions api/v1/healthz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package apiv1

import (
"encoding/json"
"net/http"
"testing"

"github.com/danielgtaylor/huma/v2/humatest"
)

func TestGetHealthz(t *testing.T) {
_, api := humatest.New(t)

Registerv1Routes(api)

resp := api.Get("/api/v1/healthz")

if resp.Code != http.StatusOK {
t.Error("Expected HTTP 200, got ", resp.Code)
}

var healthzResponse map[string]interface{}
json.Unmarshal([]byte(resp.Body.Bytes()), &healthzResponse)

if healthzResponse["status"] != "ok" {
t.Error("Response body did not match expected result")
}
}
62 changes: 62 additions & 0 deletions api/v1/notif_state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package apiv1

import (
"context"

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

type NotifStateInput struct {
Body struct {
Enabled bool `json:"enabled" enum:"true,false" doc:"Set state of notifications" required:"true"`
}
}

type NotifStateOutput struct {
Body struct {
Enabled bool `json:"enabled" enum:"true,false" doc:"Frigate-Notify enabled for notifications" default:"true"`
}
}

// GetNotifState returns whether app is enabled for sending notifications or not
func GetNotifState(ctx context.Context, input *struct{}) (*NotifStateOutput, error) {
log.Trace().
Str("uri", V1_PREFIX+"/notif_state").
Str("method", "GET").
Msg("Received API request")

resp := &NotifStateOutput{}
resp.Body.Enabled = config.Internal.Status.Notifications.Enabled

log.Trace().
Str("uri", V1_PREFIX+"/notif_state").
Interface("response_json", resp.Body).
Msg("Sent API response")

return resp, nil
}

// PostNotifState updates state to enable or disable app notifications
func PostNotifState(ctx context.Context, input *NotifStateInput) (*NotifStateOutput, error) {
log.Trace().
Str("uri", V1_PREFIX+"/notif_state").
Str("method", "POST").
Msg("Received API request")

config.Internal.Status.Notifications.Enabled = input.Body.Enabled

log.Debug().
Bool("state", input.Body.Enabled).
Msg("App state changed via API")

resp := &NotifStateOutput{}
resp.Body.Enabled = config.Internal.Status.Notifications.Enabled

log.Trace().
Str("uri", V1_PREFIX+"/notif_state").
//Interface("response_json", resp.Body).
Msg("Sent API response")

return resp, nil
}
Loading

0 comments on commit 42e5cc2

Please sign in to comment.