diff --git a/datapoint.go b/datapoint.go index 07346e8..f851a5b 100644 --- a/datapoint.go +++ b/datapoint.go @@ -21,14 +21,16 @@ const ( ) type Datapoint struct { - ID int - Group string - Name string - Datatype DatapointType - LastValue time.Time `db:"last_value"` - DatapointValueJSON []byte `db:"datapoint_value_json"` - LastDatapointValue DatapointValue - Found bool + ID int + Group string + Name string + Datatype DatapointType + LastValue time.Time `db:"last_value"` + DatapointValueJSON []byte `db:"datapoint_value_json"` + LastDatapointValue DatapointValue + Found bool + NodataProblemSeconds int `db:"nodata_problem_seconds"` + NodataIsProblem bool `db:"nodata_is_problem"` } type DatapointValue struct { @@ -56,13 +58,13 @@ func (dp DatapointValue) Value() any { // {{{ return nil } // }}} -func (dp DatapointValue) FormattedTime() string {// {{{ +func (dp DatapointValue) FormattedTime() string { // {{{ if dp.ValueDateTime.Valid { return dp.ValueDateTime.Time.Format("2006-01-02 15:04:05") } return "invalid time" -}// }}} -func (dp Datapoint) Update() (err error) {// {{{ +} // }}} +func (dp Datapoint) Update() (err error) { // {{{ name := strings.TrimSpace(dp.Name) if name == "" { err = errors.New("Name can't be empty") @@ -71,23 +73,47 @@ func (dp Datapoint) Update() (err error) {// {{{ if dp.ID == 0 { _, err = service.Db.Conn.Exec( - `INSERT INTO datapoint("group", name, datatype) VALUES($1, $2, $3)`, + `INSERT INTO datapoint("group", name, datatype) VALUES($1, $2, $3, $4)`, dp.Group, name, dp.Datatype, + dp.NodataProblemSeconds, ) } else { + /* Keep nodata_is_problem as is unless the nodata_problem_seconds is changed. + * Otherwise unnecessary nodata problems could be notified when updating unrelated + * datapoint properties. */ _, err = service.Db.Conn.Exec( - `UPDATE datapoint SET "group"=$2, name=$3, datatype=$4 WHERE id=$1`, + ` + UPDATE datapoint + SET + "group"=$2, + name=$3, + datatype=$4, + nodata_problem_seconds=$5, + nodata_is_problem = ( + CASE + WHEN $5 != nodata_problem_seconds THEN false + ELSE + nodata_is_problem + END + ) + WHERE + id=$1 + `, dp.ID, dp.Group, name, dp.Datatype, + dp.NodataProblemSeconds, ) } + if err != nil { + err = werr.Wrap(err) + } return -}// }}} +} // }}} func DatapointAdd[T any](name string, value T) (err error) { // {{{ row := service.Db.Conn.QueryRow(`SELECT id, datatype FROM datapoint WHERE name=$1`, name) @@ -120,7 +146,7 @@ func DatapointAdd[T any](name string, value T) (err error) { // {{{ return } - service.Db.Conn.Exec(`UPDATE datapoint SET last_value = NOW() WHERE name=$1`, name) + service.Db.Conn.Exec(`UPDATE datapoint SET last_value = NOW(), nodata_is_problem = false WHERE id=$1`, dpID) return } // }}} @@ -253,14 +279,14 @@ func DatapointRetrieve(id int, name string) (dp Datapoint, err error) { // {{{ return } // }}} -func DatapointDelete(id int) (err error) {// {{{ +func DatapointDelete(id int) (err error) { // {{{ _, err = service.Db.Conn.Exec(`DELETE FROM datapoint WHERE id=$1`, id) if err != nil { err = werr.Wrap(err).WithData(id) } return -}// }}} -func DatapointValues(id int) (values []DatapointValue, err error) {// {{{ +} // }}} +func DatapointValues(id int) (values []DatapointValue, err error) { // {{{ rows, err := service.Db.Conn.Queryx(`SELECT * FROM datapoint_value WHERE datapoint_id=$1 ORDER BY ts DESC LIMIT 500`, id) if err != nil { err = werr.Wrap(err).WithData(id) @@ -278,4 +304,4 @@ func DatapointValues(id int) (values []DatapointValue, err error) {// {{{ values = append(values, dpv) } return -}// }}} +} // }}} diff --git a/main.go b/main.go index 883457f..c0f557b 100644 --- a/main.go +++ b/main.go @@ -140,6 +140,8 @@ func main() { // {{{ service.Register("/configuration", false, false, pageConfiguration) service.Register("/entry/{datapoint}", false, false, entryDatapoint) + go nodataLoop() + err = service.Start() if err != nil { logger.Error("webserver", "error", werr.Wrap(err)) @@ -553,11 +555,15 @@ func pageDatapointUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { return } + var nodataSeconds int + nodataSeconds, _ = strconv.Atoi(r.FormValue("nodata_seconds")) + var dp Datapoint dp.ID = id dp.Group = r.FormValue("group") dp.Name = r.FormValue("name") dp.Datatype = DatapointType(r.FormValue("datatype")) + dp.NodataProblemSeconds = nodataSeconds err = dp.Update() if err != nil { httpError(w, werr.Wrap(err).Log()) diff --git a/nodata.go b/nodata.go new file mode 100644 index 0000000..b30a239 --- /dev/null +++ b/nodata.go @@ -0,0 +1,75 @@ +package main + +import ( + // External + werr "git.gibonuddevalla.se/go/wrappederror" + + // Standard + "database/sql" + "time" +) + +// nodataLoop checks if datapoint last_value is larger than the nodata_problem_seconds period and +// marks them as problems. They are then notified. +func nodataLoop() { + var ids []int + var err error + + // TODO - should be configurable + ticker := time.NewTicker(time.Second * 5) + for { + <-ticker.C + ids, err = nodataDatapointIDs() + if err != nil { + err = werr.Wrap(err).Log() + logger.Error("nodata", "error", err) + continue + } + + if len(ids) == 0 { + continue + } + + logger.Info("nodata", "problem_ids", ids) + } +} + +func nodataDatapointIDs() (ids []int, err error) { + ids = []int{} + + var rows *sql.Rows + rows, err = service.Db.Conn.Query(` + UPDATE datapoint + SET + nodata_is_problem = true + FROM ( + SELECT + id + FROM + datapoint + WHERE + NOT nodata_is_problem AND + extract(EPOCH from (NOW() - last_value))::int > nodata_problem_seconds + ) AS subquery + WHERE + datapoint.id = subquery.id + RETURNING + datapoint.id + + `) + if err != nil { + err = werr.Wrap(err) + return + } + defer rows.Close() + + var id int + for rows.Next() { + if err = rows.Scan(&id); err != nil { + err = werr.Wrap(err) + return + } + ids = append(ids, id) + } + return +} diff --git a/sql/00014.sql b/sql/00014.sql new file mode 100644 index 0000000..e31a5d5 --- /dev/null +++ b/sql/00014.sql @@ -0,0 +1,4 @@ +ALTER TABLE datapoint ADD COLUMN nodata_problem_seconds INT4 NOT NULL DEFAULT 0; +ALTER TABLE datapoint ADD COLUMN nodata_is_problem BOOL NOT NULL DEFAULT false; + +CREATE INDEX datapoint_last_value_idx ON public.datapoint ("last_value"); diff --git a/views/pages/datapoint_edit.gotmpl b/views/pages/datapoint_edit.gotmpl index 9e9ee66..9d65b50 100644 --- a/views/pages/datapoint_edit.gotmpl +++ b/views/pages/datapoint_edit.gotmpl @@ -25,6 +25,13 @@ +
No data
problem time
(seconds)
+
+ +
A problem is raised and notified if an entry isn't made within this time.
+
Set to 0 to disable.
+
+
{{ if eq .Data.Datapoint.ID 0 }}