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
|
|
|
|
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 {
|
|
|
|
return dp.ValueDateTime.Time
|
|
|
|
}
|
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 {
|
|
|
|
return dp.ValueDateTime.Time.Format("2006-01-02 15:04:05")
|
|
|
|
}
|
|
|
|
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-05-30 13:05:42 +02:00
|
|
|
`INSERT INTO datapoint("group", name, datatype, nodata_problem_seconds) VALUES($1, $2, $3, $4)`,
|
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-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,
|
|
|
|
nodata_problem_seconds=$5,
|
|
|
|
nodata_is_problem = (
|
|
|
|
CASE
|
|
|
|
WHEN $5 != nodata_problem_seconds THEN false
|
|
|
|
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-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-04-29 08:36:13 +02:00
|
|
|
row := service.Db.Conn.QueryRow(`SELECT id, datatype FROM datapoint WHERE name=$1`, name)
|
|
|
|
|
|
|
|
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:
|
|
|
|
_, err = service.Db.Conn.Exec(`INSERT INTO datapoint_value(datapoint_id, value_datetime) VALUES($1, $2)`, dpID, value)
|
|
|
|
}
|
|
|
|
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
|
|
|
ID int
|
|
|
|
value any
|
|
|
|
}{dpID, value})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-05-25 09:40:40 +02:00
|
|
|
service.Db.Conn.Exec(`UPDATE datapoint SET last_value = NOW(), nodata_is_problem = false WHERE id=$1`, dpID)
|
2024-05-20 18:57:47 +02:00
|
|
|
|
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
|
|
|
|
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
|
|
|
|
dp.id,
|
|
|
|
dp.name,
|
|
|
|
dp.datatype,
|
|
|
|
dp.last_value,
|
2024-05-20 19:40:19 +02:00
|
|
|
dp.group,
|
2024-05-30 13:01:17 +02:00
|
|
|
dp.nodata_problem_seconds,
|
2024-04-30 08:04:16 +02:00
|
|
|
|
|
|
|
dpv.id AS v_id,
|
|
|
|
dpv.ts,
|
|
|
|
dpv.value_int,
|
|
|
|
dpv.value_string,
|
|
|
|
dpv.value_datetime
|
|
|
|
|
|
|
|
FROM public.datapoint dp
|
|
|
|
LEFT JOIN (
|
|
|
|
SELECT
|
|
|
|
*,
|
|
|
|
row_number() OVER (PARTITION BY "datapoint_id" ORDER BY ts DESC) AS rn
|
|
|
|
FROM datapoint_value
|
|
|
|
) dpv ON dpv.datapoint_id = dp.id AND rn = 1
|
|
|
|
ORDER BY
|
2024-05-20 19:40:19 +02:00
|
|
|
dp.group ASC,
|
2024-04-30 08:04:16 +02:00
|
|
|
dp.name ASC
|
|
|
|
`)
|
|
|
|
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 {
|
2024-05-30 13:01:17 +02:00
|
|
|
ID int
|
|
|
|
Group string
|
|
|
|
Name string
|
|
|
|
Datatype DatapointType
|
|
|
|
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
|
|
|
|
dp.LastValue = res.LastValue
|
2024-05-04 22:07:41 +02:00
|
|
|
dp.Found = true
|
2024-05-30 13:01:17 +02:00
|
|
|
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 {
|
2024-05-04 22:07:41 +02:00
|
|
|
query = `SELECT *, true AS found FROM datapoint WHERE id = $1`
|
2024-04-30 08:04:16 +02:00
|
|
|
param = id
|
|
|
|
dp.ID = id
|
|
|
|
} else {
|
2024-05-04 22:07:41 +02:00
|
|
|
query = `SELECT *, true AS found FROM 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) { // {{{
|
2024-05-30 13:32:04 +02:00
|
|
|
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
|
|
|
} // }}}
|
|
|
|
func DatapointValues(id int) (values []DatapointValue, err error) { // {{{
|
2024-05-05 20:16:28 +02:00
|
|
|
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)
|
|
|
|
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
|
|
|
} // }}}
|