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 {
2024-05-30 13:01:17 +02:00
ID int
Group string
Name string
Datatype DatapointType
2024-06-24 11:18:51 +02:00
Comment string
2024-05-30 13:01:17 +02:00
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
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-07-24 16:05:30 +02:00
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 {
2024-07-24 16:05:30 +02:00
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 ) { // {{{
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
} // }}}
2024-06-27 08:59:34 +02:00
func DatapointValues ( id int , from , to time . Time ) ( values [ ] DatapointValue , err error ) { // {{{
2024-06-27 16:21:16 +02:00
_ , 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 (
`
2024-06-27 16:21:16 +02:00
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
2024-06-27 16:21:16 +02:00
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
} // }}}