smon/datapoint.go

374 lines
8.3 KiB
Go
Raw Normal View History

2024-04-29 08:36:13 +02:00
package main
import (
// External
2024-05-05 20:16:28 +02:00
werr "git.gibonuddevalla.se/go/wrappederror"
2024-04-30 08:04:16 +02:00
"github.com/jmoiron/sqlx"
2024-04-29 08:36:13 +02:00
// Standard
"database/sql"
2024-04-30 08:04:16 +02:00
"errors"
"strings"
2024-04-29 08:36:13 +02:00
"time"
)
type DatapointType string
const (
INT DatapointType = "INT"
STRING = "STRING"
DATETIME = "DATETIME"
)
type Datapoint struct {
2024-05-25 09:40:40 +02:00
ID int
Group string
Name string
Datatype DatapointType
2024-06-24 11:18:51 +02:00
Comment string
2024-05-25 09:40:40 +02:00
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"`
2024-04-29 08:36:13 +02:00
}
type DatapointValue struct {
ID int
DatapointID int `db:"datapoint_id"`
Ts time.Time
ValueInt sql.NullInt64 `db:"value_int"`
ValueString sql.NullString `db:"value_string"`
ValueDateTime sql.NullTime `db:"value_datetime"`
2024-05-01 10:02:33 +02:00
TemplateValue any
2024-04-29 08:36:13 +02:00
}
2024-04-30 08:04:16 +02:00
func (dp DatapointValue) Value() any { // {{{
2024-04-29 08:36:13 +02:00
if dp.ValueInt.Valid {
return dp.ValueInt.Int64
}
2024-04-30 08:04:16 +02:00
2024-04-29 08:36:13 +02:00
if dp.ValueString.Valid {
return dp.ValueString.String
}
2024-04-30 08:04:16 +02:00
2024-04-29 08:36:13 +02:00
if dp.ValueDateTime.Valid {
2024-07-04 16:54:23 +02:00
return dp.ValueDateTime.Time.In(smonConfig.timezoneLocation)
2024-04-29 08:36:13 +02:00
}
2024-04-30 08:04:16 +02:00
2024-04-29 08:36:13 +02:00
return nil
2024-04-30 08:04:16 +02:00
} // }}}
2024-05-25 09:40:40 +02:00
func (dp DatapointValue) FormattedTime() string { // {{{
2024-04-30 08:04:16 +02:00
if dp.ValueDateTime.Valid {
2024-07-04 16:54:23 +02:00
return dp.ValueDateTime.Time.In(smonConfig.timezoneLocation).Format("2006-01-02 15:04:05")
2024-04-30 08:04:16 +02:00
}
return "invalid time"
2024-05-25 09:40:40 +02:00
} // }}}
func (dp Datapoint) Update() (err error) { // {{{
2024-04-30 08:04:16 +02:00
name := strings.TrimSpace(dp.Name)
if name == "" {
err = errors.New("Name can't be empty")
return
}
if dp.ID == 0 {
_, err = service.Db.Conn.Exec(
2024-06-24 11:18:51 +02:00
`INSERT INTO datapoint("group", name, datatype, nodata_problem_seconds, comment) VALUES($1, $2, $3, $4, $5)`,
2024-05-20 19:40:19 +02:00
dp.Group,
2024-04-30 08:04:16 +02:00
name,
dp.Datatype,
2024-05-25 09:40:40 +02:00
dp.NodataProblemSeconds,
2024-06-24 11:18:51 +02:00
dp.Comment,
2024-04-30 08:04:16 +02:00
)
} else {
2024-05-25 09:40:40 +02:00
/* 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. */
2024-04-30 08:04:16 +02:00
_, err = service.Db.Conn.Exec(
2024-05-25 09:40:40 +02:00
`
UPDATE datapoint
SET
"group"=$2,
name=$3,
datatype=$4,
2024-06-24 11:18:51 +02:00
comment=$5,
nodata_problem_seconds=$6,
2024-05-25 09:40:40 +02:00
nodata_is_problem = (
CASE
2024-06-24 11:18:51 +02:00
WHEN $6 != nodata_problem_seconds THEN false
2024-05-25 09:40:40 +02:00
ELSE
nodata_is_problem
END
)
WHERE
id=$1
`,
2024-04-30 08:04:16 +02:00
dp.ID,
2024-05-20 19:40:19 +02:00
dp.Group,
2024-04-30 08:04:16 +02:00
name,
dp.Datatype,
2024-06-24 11:18:51 +02:00
dp.Comment,
2024-05-25 09:40:40 +02:00
dp.NodataProblemSeconds,
2024-04-30 08:04:16 +02:00
)
}
2024-05-25 09:40:40 +02:00
if err != nil {
err = werr.Wrap(err)
}
2024-04-30 08:04:16 +02:00
return
2024-05-25 09:40:40 +02:00
} // }}}
2024-04-29 08:36:13 +02:00
2024-04-30 08:04:16 +02:00
func DatapointAdd[T any](name string, value T) (err error) { // {{{
2024-06-27 13:14:37 +02:00
type dpRequest = struct {
ID int
value any
}
2024-04-29 08:36:13 +02:00
2024-06-27 13:14:37 +02:00
row := service.Db.Conn.QueryRow(`SELECT id, datatype FROM datapoint WHERE name=$1`, name)
2024-04-29 08:36:13 +02:00
var dpID int
var dpType DatapointType
err = row.Scan(&dpID, &dpType)
if err != nil {
2024-05-05 20:16:28 +02:00
err = werr.Wrap(err).WithData(struct {
2024-04-29 08:36:13 +02:00
Name string
Value any
}{name, value})
return
}
switch dpType {
case INT:
_, err = service.Db.Conn.Exec(`INSERT INTO datapoint_value(datapoint_id, value_int) VALUES($1, $2)`, dpID, value)
case STRING:
_, err = service.Db.Conn.Exec(`INSERT INTO datapoint_value(datapoint_id, value_string) VALUES($1, $2)`, dpID, value)
case DATETIME:
2024-06-27 13:14:37 +02:00
// Time value is required to be a RFC 3339 formatted time string
var t time.Time
valueStr, ok := any(value).([]byte)
if !ok {
return werr.New("DATETIME value not a string").WithData(dpRequest{dpID, value})
}
t, err = stringToTime(string(valueStr))
if err != nil {
return werr.Wrap(err).WithData(dpRequest{dpID, value}).Log()
}
_, err = service.Db.Conn.Exec(`INSERT INTO datapoint_value(datapoint_id, value_datetime) VALUES($1, $2)`, dpID, t)
2024-04-29 08:36:13 +02:00
}
if err != nil {
2024-06-27 13:14:37 +02:00
err = werr.Wrap(err).WithData(dpRequest{dpID, value})
2024-04-29 08:36:13 +02:00
return
}
return
2024-04-30 08:04:16 +02:00
} // }}}
2024-04-29 08:36:13 +02:00
2024-04-30 08:04:16 +02:00
func DatapointsRetrieve() (dps []Datapoint, err error) { // {{{
dps = []Datapoint{}
var rows *sqlx.Rows
rows, err = service.Db.Conn.Queryx(`
SELECT
2024-07-24 15:39:04 +02:00
id, name, datatype, last_value, "group", comment, nodata_problem_seconds,
last_value_id AS v_id,
CASE
WHEN last_value_id IS NULL THEN null
ELSE last_value
END AS ts,
last_value_int AS value_int,
last_value_string AS value_string,
last_value_datetime AS value_datetime
FROM datapoint
2024-04-30 08:04:16 +02:00
ORDER BY
2024-07-24 15:39:04 +02:00
"group" ASC,
name ASC
2024-04-30 08:04:16 +02:00
`)
if err != nil {
2024-05-05 20:16:28 +02:00
err = werr.Wrap(err)
2024-04-30 08:04:16 +02:00
}
defer rows.Close()
type DbRes struct {
ID int
Group string
Name string
Datatype DatapointType
2024-06-24 11:18:51 +02:00
Comment string
LastValue time.Time `db:"last_value"`
NodataProblemSeconds int `db:"nodata_problem_seconds"`
2024-04-30 08:04:16 +02:00
VID sql.NullInt64 `db:"v_id"`
Ts sql.NullTime
ValueInt sql.NullInt64 `db:"value_int"`
ValueString sql.NullString `db:"value_string"`
ValueDateTime sql.NullTime `db:"value_datetime"`
}
for rows.Next() {
dp := Datapoint{}
dpv := DatapointValue{}
res := DbRes{}
err = rows.StructScan(&res)
if err != nil {
2024-05-05 20:16:28 +02:00
err = werr.Wrap(err)
2024-04-30 08:04:16 +02:00
return
}
dp.ID = res.ID
dp.Name = res.Name
2024-05-20 19:40:19 +02:00
dp.Group = res.Group
2024-04-30 08:04:16 +02:00
dp.Datatype = res.Datatype
2024-06-24 11:18:51 +02:00
dp.Comment = res.Comment
2024-04-30 08:04:16 +02:00
dp.LastValue = res.LastValue
2024-05-04 22:07:41 +02:00
dp.Found = true
dp.NodataProblemSeconds = res.NodataProblemSeconds
2024-04-30 08:04:16 +02:00
if res.VID.Valid {
dpv.ID = int(res.VID.Int64)
dpv.Ts = res.Ts.Time
dpv.ValueInt = res.ValueInt
dpv.ValueString = res.ValueString
dpv.ValueDateTime = res.ValueDateTime
dp.LastDatapointValue = dpv
}
dps = append(dps, dp)
}
return
} // }}}
func DatapointRetrieve(id int, name string) (dp Datapoint, err error) { // {{{
var query string
var param any
if id > 0 {
query = `SELECT id, "group", name, "datatype", comment, last_value, nodata_problem_seconds, nodata_is_problem, true AS found FROM public.datapoint WHERE id = $1`
2024-04-30 08:04:16 +02:00
param = id
dp.ID = id
} else {
query = `SELECT id, "group", name, "datatype", comment, last_value, nodata_problem_seconds, nodata_is_problem, true AS found FROM public.datapoint WHERE name = $1`
2024-04-30 08:04:16 +02:00
param = name
}
row := service.Db.Conn.QueryRowx(query, param)
2024-04-29 08:36:13 +02:00
err = row.StructScan(&dp)
2024-05-04 22:07:41 +02:00
if err == sql.ErrNoRows {
dp = Datapoint{
Name: name,
}
err = nil
return
}
2024-04-29 08:36:13 +02:00
if err != nil {
2024-05-05 20:16:28 +02:00
err = werr.Wrap(err).WithData(name)
2024-04-29 08:36:13 +02:00
return
}
row = service.Db.Conn.QueryRowx(`
SELECT *
FROM datapoint_value
WHERE datapoint_id = $1
ORDER BY ts DESC
LIMIT 1
`,
dp.ID,
)
err = row.StructScan(&dp.LastDatapointValue)
2024-04-30 08:04:16 +02:00
if err == sql.ErrNoRows {
err = nil
return
}
2024-04-29 08:36:13 +02:00
if err != nil {
2024-05-05 20:16:28 +02:00
err = werr.Wrap(err).WithData(dp.ID)
2024-04-29 08:36:13 +02:00
return
}
2024-04-30 08:04:16 +02:00
2024-04-29 08:36:13 +02:00
return
2024-04-30 08:04:16 +02:00
} // }}}
2024-05-25 09:40:40 +02:00
func DatapointDelete(id int) (err error) { // {{{
var dpName string
row := service.Db.Conn.QueryRow(`SELECT name FROM public.datapoint WHERE id = $1`, id)
err = row.Scan(&dpName)
if err != nil {
err = werr.Wrap(err).WithData(id)
return
}
var rows *sql.Rows
rows, err = service.Db.Conn.Query(`SELECT name FROM public.trigger WHERE datapoints ? $1`, dpName)
if err != nil {
err = werr.Wrap(err).WithData(dpName)
return
}
defer rows.Close()
var triggerNames []string
var name string
for rows.Next() {
err = rows.Scan(&name)
if err != nil {
err = werr.Wrap(err)
return
}
triggerNames = append(triggerNames, name)
}
if len(triggerNames) > 0 {
return werr.New("Datapoint '%s' used in the following triggers: %s", dpName, strings.Join(triggerNames, ", "))
}
2024-05-02 08:59:55 +02:00
_, err = service.Db.Conn.Exec(`DELETE FROM datapoint WHERE id=$1`, id)
if err != nil {
2024-05-05 20:16:28 +02:00
err = werr.Wrap(err).WithData(id)
}
return
2024-05-25 09:40:40 +02:00
} // }}}
2024-06-27 08:59:34 +02:00
func DatapointValues(id int, from, to time.Time) (values []DatapointValue, err error) { // {{{
_, err = service.Db.Conn.Exec(`SELECT set_config('timezone', $1, false)`, smonConfig.Timezone().String())
if err != nil {
err = werr.Wrap(err).WithData(smonConfig.Timezone().String())
return
}
2024-06-27 08:59:34 +02:00
rows, err := service.Db.Conn.Queryx(
`
SELECT
id,
datapoint_id,
ts,
value_int,
value_string,
value_datetime
2024-06-27 08:59:34 +02:00
FROM datapoint_value
WHERE
datapoint_id=$1 AND
ts >= $2 AND
ts <= $3
ORDER BY
ts DESC
2024-06-27 08:59:34 +02:00
`,
id,
from,
to,
)
2024-05-05 20:16:28 +02:00
if err != nil {
err = werr.Wrap(err).WithData(id)
return
}
defer rows.Close()
for rows.Next() {
dpv := DatapointValue{}
err = rows.StructScan(&dpv)
if err != nil {
err = werr.Wrap(err).WithData(id)
return
}
values = append(values, dpv)
2024-05-02 08:59:55 +02:00
}
return
2024-05-25 09:40:40 +02:00
} // }}}