Working on alerts
This commit is contained in:
parent
56d8631f30
commit
bdeb82441d
|
|
@ -0,0 +1,14 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
func (s *Server) StartAlerts() {
|
||||||
|
ticker := time.NewTicker(time.Second * 10)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
s.Services.Alerts.EvaluateAlerts()
|
||||||
|
<-ticker.C
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ require (
|
||||||
github.com/go-playground/locales v0.14.0 // indirect
|
github.com/go-playground/locales v0.14.0 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.11.0 // indirect
|
github.com/go-playground/validator/v10 v10.11.0 // indirect
|
||||||
|
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect
|
||||||
github.com/goccy/go-json v0.9.10 // indirect
|
github.com/goccy/go-json v0.9.10 // indirect
|
||||||
github.com/golobby/cast v1.3.0 // indirect
|
github.com/golobby/cast v1.3.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh
|
||||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||||
github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
|
github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
|
||||||
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||||
|
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
|
||||||
|
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
|
||||||
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
|
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
|
||||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/goccy/go-json v0.9.10 h1:hCeNmprSNLB8B8vQKWl6DpuH0t60oEs+TAk9a7CScKc=
|
github.com/goccy/go-json v0.9.10 h1:hCeNmprSNLB8B8vQKWl6DpuH0t60oEs+TAk9a7CScKc=
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package integrations
|
||||||
|
|
||||||
|
type ContactPointMessageType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ContactPointEventAlertTriggered = "alert_triggered"
|
||||||
|
ContactPointEventTest = "test"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContactPointEvent struct {
|
||||||
|
Type ContactPointMessageType
|
||||||
|
|
||||||
|
AlertTriggeredEvent *ContactPointAlertTriggeredEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContactPointAlertTriggeredEvent struct {
|
||||||
|
AlertId int64
|
||||||
|
AlertName string
|
||||||
|
AlertValue string
|
||||||
|
SensorName string
|
||||||
|
CustomMessage string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContactPointIntegration interface {
|
||||||
|
ProcessEvent(evt *ContactPointEvent, rawConfig string) error
|
||||||
|
ValidateConfig(rawConfig string) error
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
package integrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TelegramIntegration struct{}
|
||||||
|
|
||||||
|
type TelegramNotificationChannelConfig struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
ApiKey string `json:"apiKey"`
|
||||||
|
TargetChannel int64 `json:"targetChannel"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s TelegramIntegration) ProcessEvent(evt *ContactPointEvent, rawConfig string) error {
|
||||||
|
config := TelegramNotificationChannelConfig{}
|
||||||
|
err := json.Unmarshal([]byte(rawConfig), &config)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse telegram integration config - %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bot, err := tgbotapi.NewBotAPI(config.ApiKey)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch evt.Type {
|
||||||
|
case ContactPointEventAlertTriggered:
|
||||||
|
data := evt.AlertTriggeredEvent
|
||||||
|
|
||||||
|
text := fmt.Sprintf("🚨 %s is at {value}", data.SensorName)
|
||||||
|
if data.CustomMessage != "" {
|
||||||
|
text = data.CustomMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
text = strings.Replace(text, "{value}", data.AlertValue, -1)
|
||||||
|
|
||||||
|
msg := tgbotapi.NewMessage(config.TargetChannel, text)
|
||||||
|
msg.ParseMode = "Markdown"
|
||||||
|
_, err = bot.Send(msg)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
cameraUrl := ""
|
||||||
|
|
||||||
|
if event != nil {
|
||||||
|
cameraUrl = fmt.Sprintf("%s/#/cameras/%d", evt.AppConfig.PublicUrl, event.CameraId)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch evt.Type {
|
||||||
|
case ChannelEventTest:
|
||||||
|
msg := tgbotapi.NewMessage(config.TargetChannel, "👋 Test message")
|
||||||
|
_, err = bot.Send(msg)
|
||||||
|
return err
|
||||||
|
|
||||||
|
case ChannelEventMotionStart:
|
||||||
|
msg := tgbotapi.NewMessage(config.TargetChannel, fmt.Sprintf("👁 **Motion detected** by %s. [Go to stream](%s)", camera.Name, cameraUrl))
|
||||||
|
msg.ParseMode = "Markdown"
|
||||||
|
_, err = bot.Send(msg)
|
||||||
|
return err
|
||||||
|
|
||||||
|
case ChannelEventCameraLost:
|
||||||
|
msg := tgbotapi.NewMessage(config.TargetChannel, fmt.Sprintf("🔴 Connection to camera %s **lost**. [Go to camera](%s)", camera.Name, cameraUrl))
|
||||||
|
msg.ParseMode = "Markdown"
|
||||||
|
_, err = bot.Send(msg)
|
||||||
|
return err
|
||||||
|
|
||||||
|
case ChannelEventCameraFound:
|
||||||
|
msg := tgbotapi.NewMessage(config.TargetChannel, fmt.Sprintf("🟢 Connection to camera %s **re-established**. [Go to camera](%s)", camera.Name, cameraUrl))
|
||||||
|
msg.ParseMode = "Markdown"
|
||||||
|
_, err = bot.Send(msg)
|
||||||
|
return err
|
||||||
|
|
||||||
|
case ChannelEventPreview:
|
||||||
|
if !event.Preview.Valid {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := os.Open(event.Preview.String)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open preview file - %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file := tgbotapi.FileReader{
|
||||||
|
Name: event.Preview.String,
|
||||||
|
Reader: reader,
|
||||||
|
}
|
||||||
|
|
||||||
|
previewMsg := tgbotapi.NewVideo(config.TargetChannel, file)
|
||||||
|
// previewMsg.Caption = fmt.Sprintf("Captured by %s", camera.Name)
|
||||||
|
_, err = bot.Send(previewMsg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
case ChannelEventMovie:
|
||||||
|
if !event.Movie.Valid {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := os.Open(event.Movie.String)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
videoFile := tgbotapi.FileReader{
|
||||||
|
Name: event.Movie.String,
|
||||||
|
Reader: reader,
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := tgbotapi.NewVideo(config.TargetChannel, videoFile)
|
||||||
|
// msg.Caption = fmt.Sprintf("Captured by %s", camera.Name)
|
||||||
|
_, err = bot.Send(msg)
|
||||||
|
|
||||||
|
return err
|
||||||
|
|
||||||
|
case ChannelEventPicture:
|
||||||
|
if !event.Picture.Valid {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := os.Open(event.Picture.String)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
videoFile := tgbotapi.FileReader{
|
||||||
|
Name: event.Movie.String,
|
||||||
|
Reader: reader,
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := tgbotapi.NewPhoto(config.TargetChannel, videoFile)
|
||||||
|
msg.Caption = fmt.Sprintf(`Captured by %s`, camera.Name)
|
||||||
|
|
||||||
|
_, err = bot.Send(msg)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s TelegramIntegration) ValidateConfig(rawConfig string) error {
|
||||||
|
config := TelegramNotificationChannelConfig{}
|
||||||
|
return json.Unmarshal([]byte(rawConfig), &config)
|
||||||
|
}
|
||||||
|
|
@ -52,6 +52,11 @@ func main() {
|
||||||
loginProtected.GET("/api/dashboards/:id", routes.GetDashboardById(server))
|
loginProtected.GET("/api/dashboards/:id", routes.GetDashboardById(server))
|
||||||
loginProtected.PUT("/api/dashboards/:id", routes.PutDashboard(server))
|
loginProtected.PUT("/api/dashboards/:id", routes.PutDashboard(server))
|
||||||
loginProtected.DELETE("/api/dashboards/:id", routes.DeleteDashboard(server))
|
loginProtected.DELETE("/api/dashboards/:id", routes.DeleteDashboard(server))
|
||||||
|
loginProtected.GET("/api/alerts", routes.GetAlerts(server))
|
||||||
|
loginProtected.POST("/api/alerts", routes.PostAlerts(server))
|
||||||
|
loginProtected.GET("/api/alerts/:alertId", routes.GetAlert(server))
|
||||||
|
loginProtected.PUT("/api/alerts/:alertId", routes.PutAlert(server))
|
||||||
|
loginProtected.DELETE("/api/alerts/:alertId", routes.DeleteAlert(server))
|
||||||
loginProtected.POST("/api/logout", routes.Logout(server))
|
loginProtected.POST("/api/logout", routes.Logout(server))
|
||||||
|
|
||||||
// Routes accessible using auth key
|
// Routes accessible using auth key
|
||||||
|
|
@ -61,6 +66,9 @@ func main() {
|
||||||
// Starts session cleanup goroutine
|
// Starts session cleanup goroutine
|
||||||
server.StartCleaner()
|
server.StartCleaner()
|
||||||
|
|
||||||
|
// Starts alerts handling goroutine
|
||||||
|
server.StartAlerts()
|
||||||
|
|
||||||
// Run the server
|
// Run the server
|
||||||
router.Run(fmt.Sprintf("%s:%d", server.Config.Ip, server.Config.Port))
|
router.Run(fmt.Sprintf("%s:%d", server.Config.Ip, server.Config.Port))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"basic-sensor-receiver/app"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type postAlertsBody struct {
|
||||||
|
ContactPointId int64 `json:"contactPointId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Condition string `json:"condition"`
|
||||||
|
TriggerInterval int64 `json:"triggerInterval"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type putAlertsBody struct {
|
||||||
|
ContactPointId int64 `json:"contactPointId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Condition string `json:"condition"`
|
||||||
|
TriggerInterval int64 `json:"triggerInterval"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAlerts(s *app.Server) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
alerts, err := s.Services.Alerts.GetList()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, alerts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PostAlerts(s *app.Server) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
body := postAlertsBody{}
|
||||||
|
|
||||||
|
if err := c.BindJSON(&body); err != nil {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
alert, err := s.Services.Alerts.Create(body.ContactPointId, body.Name, body.Condition, body.TriggerInterval)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, alert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PutAlert(s *app.Server) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
body := putAlertsBody{}
|
||||||
|
|
||||||
|
alertId, err := getAlertId(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.BindJSON(&body); err != nil {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
alert, err := s.Services.Alerts.Update(alertId, body.ContactPointId, body.Name, body.Condition, body.TriggerInterval)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, alert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAlert(s *app.Server) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
alertId, err := getAlertId(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
alert, err := s.Services.Alerts.GetById(alertId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, alert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteAlert(s *app.Server) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
alertId, err := getAlertId(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Services.Alerts.DeleteById(alertId); err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAlertId(c *gin.Context) (int64, error) {
|
||||||
|
id := c.Param("alertId")
|
||||||
|
|
||||||
|
return strconv.ParseInt(id, 10, 64)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
package routes
|
||||||
|
|
@ -0,0 +1,302 @@
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"basic-sensor-receiver/integrations"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AlertsService struct {
|
||||||
|
ctx *Context
|
||||||
|
}
|
||||||
|
|
||||||
|
type AlertItem struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
ContactPointId int64 `json:"contactPointId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Condition string `json:"condition"`
|
||||||
|
|
||||||
|
/* how long does the condition have to be true for the alert to go off */
|
||||||
|
TriggerInterval int64 `json:"triggerInterval"`
|
||||||
|
|
||||||
|
/* current alert status, possible values: good, pending, alerting */
|
||||||
|
LastStatus string `json:"lastStatus"`
|
||||||
|
/* time at which was status last changed */
|
||||||
|
LastStatusAt int64 `json:"lastStatusAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AlertConditionSensorValue struct {
|
||||||
|
SensorId int64 `json:"sensorId"`
|
||||||
|
Condition string `json:"condition"`
|
||||||
|
Value float64 `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AlertConditionSensorLastContact struct {
|
||||||
|
SensorId int64 `json:"sensorId"`
|
||||||
|
Value int64 `json:"value"`
|
||||||
|
ValueUnit string `json:"valueUnit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Conditions examples:
|
||||||
|
sensor value (temperature) is less/more/equal to <<number>>
|
||||||
|
last contact by sensor is more than 5 minutes ago
|
||||||
|
|
||||||
|
|
||||||
|
// When value of sensor 1 is less than 20
|
||||||
|
{
|
||||||
|
type: 'sensor_value',
|
||||||
|
sensorId: 1,
|
||||||
|
condition: 'less',
|
||||||
|
value: 20
|
||||||
|
}
|
||||||
|
|
||||||
|
// When last contact by sensor was more than 10 minutes ago
|
||||||
|
{
|
||||||
|
type: 'sensor_last_contact',
|
||||||
|
sensorId: 1,
|
||||||
|
value: 10
|
||||||
|
valueUnit: 'minutes'
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
func evaluateSensorValueCondition(condition *AlertConditionSensorValue, value *sensorValue) bool {
|
||||||
|
switch condition.Condition {
|
||||||
|
case "less":
|
||||||
|
return value.Value < condition.Value
|
||||||
|
case "more":
|
||||||
|
return value.Value > condition.Value
|
||||||
|
case "equal":
|
||||||
|
return value.Value == condition.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AlertsService) EvaluateAlerts() error {
|
||||||
|
alerts, err := s.GetList()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, alert := range alerts {
|
||||||
|
err := s.EvaluateAlert(alert)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AlertsService) EvaluateAlert(alert *AlertItem) error {
|
||||||
|
condition := map[string]interface{}{}
|
||||||
|
err := json.Unmarshal([]byte(alert.Condition), &condition)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newStatus := alert.LastStatus
|
||||||
|
lastValue := ""
|
||||||
|
conditionMet := false
|
||||||
|
sensorId := int64(-1)
|
||||||
|
|
||||||
|
switch condition["type"].(string) {
|
||||||
|
case "sensor_value":
|
||||||
|
{
|
||||||
|
conditionData := AlertConditionSensorValue{
|
||||||
|
SensorId: condition["sensorId"].(int64),
|
||||||
|
Condition: condition["condition"].(string),
|
||||||
|
Value: condition["value"].(float64),
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := s.ctx.Services.SensorValues.GetLatest(conditionData.SensorId, time.Now().Unix())
|
||||||
|
lastValue = fmt.Sprintf("%f", value.Value)
|
||||||
|
sensorId = int64(conditionData.SensorId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
conditionMet = evaluateSensorValueCondition(&conditionData, value)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
TODO:
|
||||||
|
case "sensor_last_contact":
|
||||||
|
{
|
||||||
|
conditionData := AlertConditionSensorLastContact{
|
||||||
|
SensorId: condition["sensorId"].(int64),
|
||||||
|
Value: condition["value"].(int64),
|
||||||
|
ValueUnit: condition["valueUnit"].(string),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
value, err := s.ctx.Services.Sensors.GetLastContact(conditionData.SensorId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
conditionMet := time.Now().Unix()-value > conditionData.Value
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
if conditionMet {
|
||||||
|
if newStatus == "good" {
|
||||||
|
newStatus = "alerting"
|
||||||
|
} else if newStatus == "pending" {
|
||||||
|
if time.Now().Unix()-alert.LastStatusAt > alert.TriggerInterval {
|
||||||
|
newStatus = "alerting"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if newStatus != alert.LastStatus {
|
||||||
|
s.ctx.DB.Exec("UPDATE alerts SET last_status = ?, last_status_at = ? WHERE id = ?", newStatus, time.Now().Unix(), alert.Id)
|
||||||
|
|
||||||
|
sensor, err := s.ctx.Services.Sensors.GetById(sensorId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
contactPoint, err := s.ctx.Services.ContactPoints.GetById(alert.ContactPointId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchService, err := contactPoint.getService(s.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dispatchService.ProcessEvent(&integrations.ContactPointEvent{
|
||||||
|
Type: "alert",
|
||||||
|
AlertTriggeredEvent: &integrations.ContactPointAlertTriggeredEvent{
|
||||||
|
AlertId: alert.Id,
|
||||||
|
AlertName: alert.Name,
|
||||||
|
AlertValue: lastValue,
|
||||||
|
SensorName: sensor.Name,
|
||||||
|
CustomMessage: "",
|
||||||
|
},
|
||||||
|
}, contactPoint.TypeConfig)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AlertsService) GetList() ([]*AlertItem, error) {
|
||||||
|
rows, err := s.ctx.DB.Query("SELECT id, name, condition, trigger_interval, last_status, last_status_at FROM alerts ORDER BY name ASC")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
alerts := []*AlertItem{}
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
alert := AlertItem{}
|
||||||
|
|
||||||
|
err := rows.Scan(&alert.Id, &alert.Name, &alert.Condition, &alert.TriggerInterval, &alert.LastStatus, &alert.LastStatusAt)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
alerts = append(alerts, &alert)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return alerts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AlertsService) GetById(id int64) (*AlertItem, error) {
|
||||||
|
alert := AlertItem{}
|
||||||
|
|
||||||
|
row := s.ctx.DB.QueryRow("SELECT id, name, condition, trigger_interval, last_status, last_status_at FROM alerts WHERE id = ?", id)
|
||||||
|
err := row.Scan(&alert.Id, &alert.Name, &alert.Condition, &alert.TriggerInterval, &alert.LastStatus, &alert.LastStatusAt)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &alert, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AlertsService) Create(contactPointId int64, name string, condition string, triggerInterval int64) (*AlertItem, error) {
|
||||||
|
alert := AlertItem{
|
||||||
|
ContactPointId: contactPointId,
|
||||||
|
Name: name,
|
||||||
|
Condition: condition,
|
||||||
|
TriggerInterval: triggerInterval,
|
||||||
|
LastStatus: "good",
|
||||||
|
LastStatusAt: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := s.ctx.DB.Exec(
|
||||||
|
"INSERT INTO alerts (contact_point_id, name, condition, trigger_interval, last_status, last_status_at) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
alert.ContactPointId, alert.Name, alert.Condition, alert.TriggerInterval, alert.LastStatus, alert.LastStatusAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
alert.Id, err = res.LastInsertId()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &alert, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AlertsService) DeleteById(id int64) error {
|
||||||
|
_, err := s.ctx.DB.Exec("DELETE FROM alerts WHERE id = ?", id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AlertsService) Update(id int64, contactPointId int64, name string, condition string, triggerInterval int64) (*AlertItem, error) {
|
||||||
|
alert := AlertItem{
|
||||||
|
Id: id,
|
||||||
|
ContactPointId: contactPointId,
|
||||||
|
Name: name,
|
||||||
|
Condition: condition,
|
||||||
|
TriggerInterval: triggerInterval,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := s.ctx.DB.Exec(
|
||||||
|
"UPDATE alerts SET contact_point_id = ?, name = ?, condition = ?, trigger_interval = ? WHERE id = ?",
|
||||||
|
alert.ContactPointId, alert.Name, alert.Condition, alert.TriggerInterval, alert.Id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &alert, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"basic-sensor-receiver/integrations"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContactPointsService struct {
|
||||||
|
ctx *Context
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContactPointItem struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
TypeConfig string `json:"typeConfig"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContactPointsService) GetList() ([]ContactPointItem, error) {
|
||||||
|
contactPoints := make([]ContactPointItem, 0)
|
||||||
|
|
||||||
|
rows, err := s.ctx.DB.Query("SELECT id, name, type, type_config FROM contact_points")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return contactPoints, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
item := ContactPointItem{}
|
||||||
|
|
||||||
|
err := rows.Scan(&item.Id, &item.Name, &item.Type, &item.TypeConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
contactPoints = append(contactPoints, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return contactPoints, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContactPointsService) Create(name string, contactType string, contactTypeConfig string) (*ContactPointItem, error) {
|
||||||
|
switch contactType {
|
||||||
|
case "telegram":
|
||||||
|
{
|
||||||
|
err := s.ctx.Integrations.Telegram.ValidateConfig(contactTypeConfig)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to validate telegram config - %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item := ContactPointItem{
|
||||||
|
Name: name,
|
||||||
|
Type: contactType,
|
||||||
|
TypeConfig: contactTypeConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := s.ctx.DB.Exec("INSERT INTO contact_points (name, type, type_config) VALUES (?, ?, ?)", item.Name, item.Type, item.TypeConfig)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := res.LastInsertId()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
item.Id = id
|
||||||
|
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContactPointsService) GetById(id int64) (*ContactPointItem, error) {
|
||||||
|
item := ContactPointItem{}
|
||||||
|
|
||||||
|
row := s.ctx.DB.QueryRow("SELECT id, name, type, type_config FROM contact_points WHERE id = ?", id)
|
||||||
|
err := row.Scan(&item.Id, &item.Name, &item.Type, &item.TypeConfig)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContactPointsService) Update(id int64, name string, contactType string, contactTypeConfig string) (*ContactPointItem, error) {
|
||||||
|
item := ContactPointItem{
|
||||||
|
Id: id,
|
||||||
|
Name: name,
|
||||||
|
Type: contactType,
|
||||||
|
TypeConfig: contactTypeConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := s.ctx.DB.Exec("UPDATE contact_points SET name = ?, type = ?, type_config = ? WHERE id = ?", item.Name, item.Type, item.TypeConfig, item.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContactPointsService) Delete(id int64) error {
|
||||||
|
_, err := s.ctx.DB.Exec("DELETE FROM contact_points WHERE id = ?", id)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContactPointsService) Test(item *ContactPointItem) error {
|
||||||
|
service, err := item.getTestService(s.ctx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return service.ProcessEvent(&integrations.ContactPointEvent{
|
||||||
|
Type: integrations.ContactPointEventTest,
|
||||||
|
}, item.TypeConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (channel *ContactPointItem) getService(ctx *Context) (integrations.ContactPointIntegration, error) {
|
||||||
|
switch channel.Type {
|
||||||
|
case "telegram":
|
||||||
|
return ctx.Integrations.Telegram, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("unknown channel type")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (channel *ContactPointItem) getTestService(ctx *Context) (integrations.ContactPointIntegration, error) {
|
||||||
|
switch channel.Type {
|
||||||
|
case "telegram":
|
||||||
|
return ctx.Integrations.Telegram, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("unknown channel type")
|
||||||
|
}
|
||||||
|
|
@ -2,22 +2,30 @@ package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"basic-sensor-receiver/config"
|
"basic-sensor-receiver/config"
|
||||||
|
"basic-sensor-receiver/integrations"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Services struct {
|
type Services struct {
|
||||||
SensorConfig *SensorConfigService
|
SensorConfig *SensorConfigService
|
||||||
SensorValues *SensorValuesService
|
SensorValues *SensorValuesService
|
||||||
Sensors *SensorsService
|
Sensors *SensorsService
|
||||||
Sessions *SessionsService
|
Sessions *SessionsService
|
||||||
Auth *AuthService
|
Auth *AuthService
|
||||||
Dashboards *DashboardsService
|
Dashboards *DashboardsService
|
||||||
|
Alerts *AlertsService
|
||||||
|
ContactPoints *ContactPointsService
|
||||||
|
}
|
||||||
|
|
||||||
|
type Integrations struct {
|
||||||
|
Telegram *integrations.TelegramIntegration
|
||||||
}
|
}
|
||||||
|
|
||||||
type Context struct {
|
type Context struct {
|
||||||
DB *sql.DB
|
DB *sql.DB
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
Services *Services
|
Services *Services
|
||||||
|
Integrations *Integrations
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitializeServices(ctx *Context) *Services {
|
func InitializeServices(ctx *Context) *Services {
|
||||||
|
|
@ -31,6 +39,11 @@ func InitializeServices(ctx *Context) *Services {
|
||||||
services.Sessions = &SessionsService{ctx: ctx}
|
services.Sessions = &SessionsService{ctx: ctx}
|
||||||
services.Auth = &AuthService{ctx: ctx}
|
services.Auth = &AuthService{ctx: ctx}
|
||||||
services.Dashboards = &DashboardsService{ctx: ctx}
|
services.Dashboards = &DashboardsService{ctx: ctx}
|
||||||
|
services.Alerts = &AlertsService{ctx: ctx}
|
||||||
|
services.ContactPoints = &ContactPointsService{ctx: ctx}
|
||||||
|
|
||||||
|
ctx.Integrations = &Integrations{}
|
||||||
|
ctx.Integrations.Telegram = &integrations.TelegramIntegration{}
|
||||||
|
|
||||||
return &services
|
return &services
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue