evcc-io/core/metrics/db.go

84 lines
1.9 KiB
Go

package metrics
import (
"errors"
"time"
"github.com/evcc-io/evcc/server/db"
"github.com/evcc-io/evcc/tariff"
"gorm.io/gorm"
)
type meter struct {
Meter int `json:"meter" gorm:"column:meter;uniqueIndex:meter_ts"`
Timestamp time.Time `json:"ts" gorm:"column:ts;uniqueIndex:meter_ts"`
Value float64 `json:"val" gorm:"column:val"`
}
var ErrIncomplete = errors.New("meter profile incomplete")
func init() {
db.Register(func(db *gorm.DB) error {
return db.AutoMigrate(new(meter))
})
}
// Persist stores 15min consumption in Wh
func Persist(ts time.Time, value float64) error {
return db.Instance.Create(meter{
Meter: 1,
Timestamp: ts.Truncate(15 * time.Minute),
Value: value,
}).Error
}
// Profile returns a 15min average meter profile in Wh.
// Profile is sorted by timestamp starting at 00:00. It is guaranteed to contain 96 15min values.
func Profile(from time.Time) (*[96]float64, error) {
db, err := db.Instance.DB()
if err != nil {
return nil, err
}
// Use 'localtime' in strftime to fix https://github.com/evcc-io/evcc/discussions/23759
rows, err := db.Query(`SELECT min(ts) AS ts, avg(val) AS val
FROM meters
WHERE meter = ? AND ts >= ?
GROUP BY strftime("%H:%M", ts, 'localtime')
ORDER BY strftime("%H:%M", ts, 'localtime') ASC`, 1, from,
)
if err != nil {
return nil, err
}
defer rows.Close()
var prev time.Time
res := make([]float64, 0, 96)
for rows.Next() {
var ts SqlTime
var val float64
if err := rows.Scan(&ts, &val); err != nil {
return nil, err
}
// interpolate single missing value, maybe due to regular restarts?
if time.Time(ts).Sub(prev) == 2*tariff.SlotDuration {
res = append(res, (val+res[len(res)-1])/2)
}
prev = time.Time(ts)
res = append(res, val)
}
if err := rows.Err(); err != nil {
return nil, err
}
if len(res) != 96 {
return nil, ErrIncomplete
}
return (*[96]float64)(res), nil
}