Continued work on alerts

This commit is contained in:
Jan Zípek 2024-03-29 20:49:08 +01:00
parent bdeb82441d
commit 3b0c3fff64
Signed by: kamen
GPG Key ID: A17882625B33AC31
6 changed files with 115 additions and 216 deletions

View File

@ -1,5 +1,7 @@
package integrations package integrations
import "basic-sensor-receiver/models"
type ContactPointMessageType string type ContactPointMessageType string
const ( const (
@ -14,11 +16,11 @@ type ContactPointEvent struct {
} }
type ContactPointAlertTriggeredEvent struct { type ContactPointAlertTriggeredEvent struct {
AlertId int64 Alert *models.AlertItem
AlertName string Sensor *models.SensorItem
AlertValue string SensorValueCondition *models.AlertConditionSensorValue
SensorName string SensorLastContactCondition *models.AlertConditionSensorLastContact
CustomMessage string LastValue float64
} }
type ContactPointIntegration interface { type ContactPointIntegration interface {

View File

@ -34,12 +34,13 @@ func (s TelegramIntegration) ProcessEvent(evt *ContactPointEvent, rawConfig stri
case ContactPointEventAlertTriggered: case ContactPointEventAlertTriggered:
data := evt.AlertTriggeredEvent data := evt.AlertTriggeredEvent
text := fmt.Sprintf("🚨 %s is at {value}", data.SensorName) text := fmt.Sprintf("🚨 %s is at {value}", data.Sensor.Name)
if data.CustomMessage != "" {
text = data.CustomMessage if data.Alert.CustomMessage != "" {
text = data.Alert.CustomMessage
} }
text = strings.Replace(text, "{value}", data.AlertValue, -1) text = strings.Replace(text, "{value}", fmt.Sprintf("%f", data.LastValue), -1)
msg := tgbotapi.NewMessage(config.TargetChannel, text) msg := tgbotapi.NewMessage(config.TargetChannel, text)
msg.ParseMode = "Markdown" msg.ParseMode = "Markdown"
@ -47,108 +48,6 @@ func (s TelegramIntegration) ProcessEvent(evt *ContactPointEvent, rawConfig stri
return err 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 return nil
} }

29
server/models/alerts.go Normal file
View File

@ -0,0 +1,29 @@
package models
type AlertItem struct {
Id int64 `json:"id"`
ContactPointId int64 `json:"contactPointId"`
Name string `json:"name"`
Condition string `json:"condition"`
CustomMessage string `json:"customMessage"`
/* 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"`
}

7
server/models/sensors.go Normal file
View File

@ -0,0 +1,7 @@
package models
type SensorItem struct {
Id int64 `json:"id"`
Name string `json:"name"`
AuthKey string `json:"authKey"`
}

View File

@ -2,8 +2,8 @@ package services
import ( import (
"basic-sensor-receiver/integrations" "basic-sensor-receiver/integrations"
"basic-sensor-receiver/models"
"encoding/json" "encoding/json"
"fmt"
"time" "time"
) )
@ -11,58 +11,7 @@ type AlertsService struct {
ctx *Context ctx *Context
} }
type AlertItem struct { func evaluateSensorValueCondition(condition *models.AlertConditionSensorValue, value *sensorValue) bool {
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 { switch condition.Condition {
case "less": case "less":
return value.Value < condition.Value return value.Value < condition.Value
@ -94,7 +43,22 @@ func (s *AlertsService) EvaluateAlerts() error {
} }
func (s *AlertsService) EvaluateAlert(alert *AlertItem) error { func unitToSeconds(unit string) int64 {
switch unit {
case "s":
return 1
case "m":
return 60
case "h":
return 3600
case "d":
return 86400
}
return 0
}
func (s *AlertsService) EvaluateAlert(alert *models.AlertItem) error {
condition := map[string]interface{}{} condition := map[string]interface{}{}
err := json.Unmarshal([]byte(alert.Condition), &condition) err := json.Unmarshal([]byte(alert.Condition), &condition)
if err != nil { if err != nil {
@ -102,53 +66,56 @@ func (s *AlertsService) EvaluateAlert(alert *AlertItem) error {
} }
newStatus := alert.LastStatus newStatus := alert.LastStatus
lastValue := "" lastValue := float64(0)
conditionMet := false conditionMet := false
sensorId := int64(-1) sensorId := int64(-1)
sensorValueCondition := models.AlertConditionSensorValue{}
sensorLastContactCondition := models.AlertConditionSensorLastContact{}
switch condition["type"].(string) { switch condition["type"].(string) {
case "sensor_value": case "sensor_value":
{ {
conditionData := AlertConditionSensorValue{ sensorValueCondition = models.AlertConditionSensorValue{
SensorId: condition["sensorId"].(int64), SensorId: condition["sensorId"].(int64),
Condition: condition["condition"].(string), Condition: condition["condition"].(string),
Value: condition["value"].(float64), Value: condition["value"].(float64),
} }
value, err := s.ctx.Services.SensorValues.GetLatest(conditionData.SensorId, time.Now().Unix()) value, err := s.ctx.Services.SensorValues.GetLatest(sensorValueCondition.SensorId, time.Now().Unix())
lastValue = fmt.Sprintf("%f", value.Value) lastValue = value.Value
sensorId = int64(conditionData.SensorId) sensorId = int64(sensorValueCondition.SensorId)
if err != nil { if err != nil {
return err return err
} }
conditionMet = evaluateSensorValueCondition(&conditionData, value) conditionMet = evaluateSensorValueCondition(&sensorValueCondition, value)
break break
} }
/*
TODO:
case "sensor_last_contact":
{
conditionData := AlertConditionSensorLastContact{
SensorId: condition["sensorId"].(int64),
Value: condition["value"].(int64),
ValueUnit: condition["valueUnit"].(string),
}
case "sensor_last_contact":
{
sensorLastContactCondition := models.AlertConditionSensorLastContact{
SensorId: condition["sensorId"].(int64),
Value: condition["value"].(int64),
ValueUnit: condition["valueUnit"].(string),
}
value, err := s.ctx.Services.Sensors.GetLastContact(conditionData.SensorId) value, err := s.ctx.Services.SensorValues.GetLatest(sensorLastContactCondition.SensorId, time.Now().Unix())
lastValue = float64(value.Timestamp)
if err != nil { if err != nil {
return err return err
} }
conditionMet := time.Now().Unix()-value > conditionData.Value conditionInSec := sensorLastContactCondition.Value * unitToSeconds(sensorLastContactCondition.ValueUnit)
conditionMet = time.Now().Unix()-value.Timestamp > conditionInSec
break
}
break
}
*/
} }
if conditionMet { if conditionMet {
@ -182,11 +149,11 @@ func (s *AlertsService) EvaluateAlert(alert *AlertItem) error {
err = dispatchService.ProcessEvent(&integrations.ContactPointEvent{ err = dispatchService.ProcessEvent(&integrations.ContactPointEvent{
Type: "alert", Type: "alert",
AlertTriggeredEvent: &integrations.ContactPointAlertTriggeredEvent{ AlertTriggeredEvent: &integrations.ContactPointAlertTriggeredEvent{
AlertId: alert.Id, Alert: alert,
AlertName: alert.Name, Sensor: sensor,
AlertValue: lastValue, SensorValueCondition: &sensorValueCondition,
SensorName: sensor.Name, SensorLastContactCondition: &sensorLastContactCondition,
CustomMessage: "", LastValue: lastValue,
}, },
}, contactPoint.TypeConfig) }, contactPoint.TypeConfig)
@ -198,7 +165,7 @@ func (s *AlertsService) EvaluateAlert(alert *AlertItem) error {
return nil return nil
} }
func (s *AlertsService) GetList() ([]*AlertItem, error) { func (s *AlertsService) GetList() ([]*models.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") 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 { if err != nil {
@ -207,10 +174,10 @@ func (s *AlertsService) GetList() ([]*AlertItem, error) {
defer rows.Close() defer rows.Close()
alerts := []*AlertItem{} alerts := []*models.AlertItem{}
for rows.Next() { for rows.Next() {
alert := AlertItem{} alert := models.AlertItem{}
err := rows.Scan(&alert.Id, &alert.Name, &alert.Condition, &alert.TriggerInterval, &alert.LastStatus, &alert.LastStatusAt) err := rows.Scan(&alert.Id, &alert.Name, &alert.Condition, &alert.TriggerInterval, &alert.LastStatus, &alert.LastStatusAt)
@ -229,8 +196,8 @@ func (s *AlertsService) GetList() ([]*AlertItem, error) {
return alerts, nil return alerts, nil
} }
func (s *AlertsService) GetById(id int64) (*AlertItem, error) { func (s *AlertsService) GetById(id int64) (*models.AlertItem, error) {
alert := AlertItem{} alert := models.AlertItem{}
row := s.ctx.DB.QueryRow("SELECT id, name, condition, trigger_interval, last_status, last_status_at FROM alerts WHERE id = ?", id) 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) err := row.Scan(&alert.Id, &alert.Name, &alert.Condition, &alert.TriggerInterval, &alert.LastStatus, &alert.LastStatusAt)
@ -242,8 +209,8 @@ func (s *AlertsService) GetById(id int64) (*AlertItem, error) {
return &alert, nil return &alert, nil
} }
func (s *AlertsService) Create(contactPointId int64, name string, condition string, triggerInterval int64) (*AlertItem, error) { func (s *AlertsService) Create(contactPointId int64, name string, condition string, triggerInterval int64) (*models.AlertItem, error) {
alert := AlertItem{ alert := models.AlertItem{
ContactPointId: contactPointId, ContactPointId: contactPointId,
Name: name, Name: name,
Condition: condition, Condition: condition,
@ -280,8 +247,8 @@ func (s *AlertsService) DeleteById(id int64) error {
return nil return nil
} }
func (s *AlertsService) Update(id int64, contactPointId int64, name string, condition string, triggerInterval int64) (*AlertItem, error) { func (s *AlertsService) Update(id int64, contactPointId int64, name string, condition string, triggerInterval int64) (*models.AlertItem, error) {
alert := AlertItem{ alert := models.AlertItem{
Id: id, Id: id,
ContactPointId: contactPointId, ContactPointId: contactPointId,
Name: name, Name: name,

View File

@ -1,6 +1,7 @@
package services package services
import ( import (
"basic-sensor-receiver/models"
"crypto/rand" "crypto/rand"
"database/sql" "database/sql"
"math/big" "math/big"
@ -10,14 +11,8 @@ type SensorsService struct {
ctx *Context ctx *Context
} }
type SensorItem struct { func (s *SensorsService) GetList() ([]models.SensorItem, error) {
Id int64 `json:"id"` sensors := make([]models.SensorItem, 0)
Name string `json:"name"`
AuthKey string `json:"authKey"`
}
func (s *SensorsService) GetList() ([]SensorItem, error) {
sensors := make([]SensorItem, 0)
rows, err := s.ctx.DB.Query("SELECT id, name, auth_key FROM sensors") rows, err := s.ctx.DB.Query("SELECT id, name, auth_key FROM sensors")
@ -32,7 +27,7 @@ func (s *SensorsService) GetList() ([]SensorItem, error) {
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
item := SensorItem{} item := models.SensorItem{}
err := rows.Scan(&item.Id, &item.Name, &item.AuthKey) err := rows.Scan(&item.Id, &item.Name, &item.AuthKey)
if err != nil { if err != nil {
@ -50,14 +45,14 @@ func (s *SensorsService) GetList() ([]SensorItem, error) {
return sensors, nil return sensors, nil
} }
func (s *SensorsService) Create(name string) (*SensorItem, error) { func (s *SensorsService) Create(name string) (*models.SensorItem, error) {
authKey, err := generateRandomString(32) authKey, err := generateRandomString(32)
if err != nil { if err != nil {
return nil, err return nil, err
} }
item := SensorItem{ item := models.SensorItem{
Name: name, Name: name,
AuthKey: authKey, AuthKey: authKey,
} }
@ -77,8 +72,8 @@ func (s *SensorsService) Create(name string) (*SensorItem, error) {
return &item, nil return &item, nil
} }
func (s *SensorsService) GetById(id int64) (*SensorItem, error) { func (s *SensorsService) GetById(id int64) (*models.SensorItem, error) {
item := SensorItem{} item := models.SensorItem{}
row := s.ctx.DB.QueryRow("SELECT id, name, auth_key FROM sensors WHERE id = ?", id) row := s.ctx.DB.QueryRow("SELECT id, name, auth_key FROM sensors WHERE id = ?", id)
@ -90,7 +85,7 @@ func (s *SensorsService) GetById(id int64) (*SensorItem, error) {
return &item, nil return &item, nil
} }
func (s *SensorsService) Update(id int64, name string) (*SensorItem, error) { func (s *SensorsService) Update(id int64, name string) (*models.SensorItem, error) {
item, err := s.GetById(id) item, err := s.GetById(id)
if err != nil { if err != nil {