diff --git a/server/integrations/integrations.go b/server/integrations/integrations.go index 150727d..7d0b75e 100644 --- a/server/integrations/integrations.go +++ b/server/integrations/integrations.go @@ -1,5 +1,7 @@ package integrations +import "basic-sensor-receiver/models" + type ContactPointMessageType string const ( @@ -14,11 +16,11 @@ type ContactPointEvent struct { } type ContactPointAlertTriggeredEvent struct { - AlertId int64 - AlertName string - AlertValue string - SensorName string - CustomMessage string + Alert *models.AlertItem + Sensor *models.SensorItem + SensorValueCondition *models.AlertConditionSensorValue + SensorLastContactCondition *models.AlertConditionSensorLastContact + LastValue float64 } type ContactPointIntegration interface { diff --git a/server/integrations/telegram.go b/server/integrations/telegram.go index aa5b59f..43f8fdf 100644 --- a/server/integrations/telegram.go +++ b/server/integrations/telegram.go @@ -34,12 +34,13 @@ func (s TelegramIntegration) ProcessEvent(evt *ContactPointEvent, rawConfig stri case ContactPointEventAlertTriggered: data := evt.AlertTriggeredEvent - text := fmt.Sprintf("🚨 %s is at {value}", data.SensorName) - if data.CustomMessage != "" { - text = data.CustomMessage + text := fmt.Sprintf("🚨 %s is at {value}", data.Sensor.Name) + + 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.ParseMode = "Markdown" @@ -47,108 +48,6 @@ func (s TelegramIntegration) ProcessEvent(evt *ContactPointEvent, rawConfig stri 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 } diff --git a/server/models/alerts.go b/server/models/alerts.go new file mode 100644 index 0000000..d3fae01 --- /dev/null +++ b/server/models/alerts.go @@ -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"` +} diff --git a/server/models/sensors.go b/server/models/sensors.go new file mode 100644 index 0000000..c874cb3 --- /dev/null +++ b/server/models/sensors.go @@ -0,0 +1,7 @@ +package models + +type SensorItem struct { + Id int64 `json:"id"` + Name string `json:"name"` + AuthKey string `json:"authKey"` +} diff --git a/server/services/alerts_service.go b/server/services/alerts_service.go index 79bc6a0..3de578e 100644 --- a/server/services/alerts_service.go +++ b/server/services/alerts_service.go @@ -2,8 +2,8 @@ package services import ( "basic-sensor-receiver/integrations" + "basic-sensor-receiver/models" "encoding/json" - "fmt" "time" ) @@ -11,58 +11,7 @@ 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 <> - 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 { +func evaluateSensorValueCondition(condition *models.AlertConditionSensorValue, value *sensorValue) bool { switch condition.Condition { case "less": 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{}{} err := json.Unmarshal([]byte(alert.Condition), &condition) if err != nil { @@ -102,53 +66,56 @@ func (s *AlertsService) EvaluateAlert(alert *AlertItem) error { } newStatus := alert.LastStatus - lastValue := "" + lastValue := float64(0) conditionMet := false sensorId := int64(-1) + sensorValueCondition := models.AlertConditionSensorValue{} + sensorLastContactCondition := models.AlertConditionSensorLastContact{} switch condition["type"].(string) { case "sensor_value": { - conditionData := AlertConditionSensorValue{ + sensorValueCondition = models.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) + value, err := s.ctx.Services.SensorValues.GetLatest(sensorValueCondition.SensorId, time.Now().Unix()) + lastValue = value.Value + sensorId = int64(sensorValueCondition.SensorId) if err != nil { return err } - conditionMet = evaluateSensorValueCondition(&conditionData, value) + conditionMet = evaluateSensorValueCondition(&sensorValueCondition, value) 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 { - return err - } + if err != nil { + 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 { @@ -182,11 +149,11 @@ func (s *AlertsService) EvaluateAlert(alert *AlertItem) error { err = dispatchService.ProcessEvent(&integrations.ContactPointEvent{ Type: "alert", AlertTriggeredEvent: &integrations.ContactPointAlertTriggeredEvent{ - AlertId: alert.Id, - AlertName: alert.Name, - AlertValue: lastValue, - SensorName: sensor.Name, - CustomMessage: "", + Alert: alert, + Sensor: sensor, + SensorValueCondition: &sensorValueCondition, + SensorLastContactCondition: &sensorLastContactCondition, + LastValue: lastValue, }, }, contactPoint.TypeConfig) @@ -198,7 +165,7 @@ func (s *AlertsService) EvaluateAlert(alert *AlertItem) error { 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") if err != nil { @@ -207,10 +174,10 @@ func (s *AlertsService) GetList() ([]*AlertItem, error) { defer rows.Close() - alerts := []*AlertItem{} + alerts := []*models.AlertItem{} for rows.Next() { - alert := AlertItem{} + alert := models.AlertItem{} 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 } -func (s *AlertsService) GetById(id int64) (*AlertItem, error) { - alert := AlertItem{} +func (s *AlertsService) GetById(id int64) (*models.AlertItem, error) { + alert := models.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) @@ -242,8 +209,8 @@ func (s *AlertsService) GetById(id int64) (*AlertItem, error) { return &alert, nil } -func (s *AlertsService) Create(contactPointId int64, name string, condition string, triggerInterval int64) (*AlertItem, error) { - alert := AlertItem{ +func (s *AlertsService) Create(contactPointId int64, name string, condition string, triggerInterval int64) (*models.AlertItem, error) { + alert := models.AlertItem{ ContactPointId: contactPointId, Name: name, Condition: condition, @@ -280,8 +247,8 @@ func (s *AlertsService) DeleteById(id int64) error { return nil } -func (s *AlertsService) Update(id int64, contactPointId int64, name string, condition string, triggerInterval int64) (*AlertItem, error) { - alert := AlertItem{ +func (s *AlertsService) Update(id int64, contactPointId int64, name string, condition string, triggerInterval int64) (*models.AlertItem, error) { + alert := models.AlertItem{ Id: id, ContactPointId: contactPointId, Name: name, diff --git a/server/services/sensors_service.go b/server/services/sensors_service.go index 712ab71..3e62f0c 100644 --- a/server/services/sensors_service.go +++ b/server/services/sensors_service.go @@ -1,6 +1,7 @@ package services import ( + "basic-sensor-receiver/models" "crypto/rand" "database/sql" "math/big" @@ -10,14 +11,8 @@ type SensorsService struct { ctx *Context } -type SensorItem struct { - Id int64 `json:"id"` - Name string `json:"name"` - AuthKey string `json:"authKey"` -} - -func (s *SensorsService) GetList() ([]SensorItem, error) { - sensors := make([]SensorItem, 0) +func (s *SensorsService) GetList() ([]models.SensorItem, error) { + sensors := make([]models.SensorItem, 0) 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() for rows.Next() { - item := SensorItem{} + item := models.SensorItem{} err := rows.Scan(&item.Id, &item.Name, &item.AuthKey) if err != nil { @@ -50,14 +45,14 @@ func (s *SensorsService) GetList() ([]SensorItem, error) { return sensors, nil } -func (s *SensorsService) Create(name string) (*SensorItem, error) { +func (s *SensorsService) Create(name string) (*models.SensorItem, error) { authKey, err := generateRandomString(32) if err != nil { return nil, err } - item := SensorItem{ + item := models.SensorItem{ Name: name, AuthKey: authKey, } @@ -77,8 +72,8 @@ func (s *SensorsService) Create(name string) (*SensorItem, error) { return &item, nil } -func (s *SensorsService) GetById(id int64) (*SensorItem, error) { - item := SensorItem{} +func (s *SensorsService) GetById(id int64) (*models.SensorItem, error) { + item := models.SensorItem{} 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 } -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) if err != nil {