package main import ( // External werr "git.gibonuddevalla.se/go/wrappederror" "github.com/jmoiron/sqlx" // Standard "database/sql" "errors" "strings" "time" ) type DatapointType string const ( INT DatapointType = "INT" STRING = "STRING" DATETIME = "DATETIME" ) 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 NodataProblemSeconds int `db:"nodata_problem_seconds"` NodataIsProblem bool `db:"nodata_is_problem"` } 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"` TemplateValue any } func (dp DatapointValue) Value() any { // {{{ if dp.ValueInt.Valid { return dp.ValueInt.Int64 } if dp.ValueString.Valid { return dp.ValueString.String } if dp.ValueDateTime.Valid { return dp.ValueDateTime.Time } return nil } // }}} 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) { // {{{ 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( `INSERT INTO datapoint("group", name, datatype, nodata_problem_seconds) 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, 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) var dpID int var dpType DatapointType err = row.Scan(&dpID, &dpType) if err != nil { err = werr.Wrap(err).WithData(struct { 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 { err = werr.Wrap(err).WithData(struct { ID int value any }{dpID, value}) return } service.Db.Conn.Exec(`UPDATE datapoint SET last_value = NOW(), nodata_is_problem = false WHERE id=$1`, dpID) return } // }}} 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, dp.group, dp.nodata_problem_seconds, 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 dp.group ASC, dp.name ASC `) if err != nil { err = werr.Wrap(err) } defer rows.Close() type DbRes struct { ID int Group string Name string Datatype DatapointType LastValue time.Time `db:"last_value"` NodataProblemSeconds int `db:"nodata_problem_seconds"` 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 { err = werr.Wrap(err) return } dp.ID = res.ID dp.Name = res.Name dp.Group = res.Group dp.Datatype = res.Datatype dp.LastValue = res.LastValue dp.Found = true dp.NodataProblemSeconds = res.NodataProblemSeconds 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 *, true AS found FROM datapoint WHERE id = $1` param = id dp.ID = id } else { query = `SELECT *, true AS found FROM datapoint WHERE name = $1` param = name } row := service.Db.Conn.QueryRowx(query, param) err = row.StructScan(&dp) if err == sql.ErrNoRows { dp = Datapoint{ Name: name, } err = nil return } if err != nil { err = werr.Wrap(err).WithData(name) 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) if err == sql.ErrNoRows { err = nil return } if err != nil { err = werr.Wrap(err).WithData(dp.ID) return } return } // }}} 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, ", ")) } _, 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) { // {{{ 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) } return } // }}}