evcc-io/core/metrics/db_profile.go

63 lines
1.4 KiB
Go

package metrics
import (
"errors"
"time"
"github.com/evcc-io/evcc/server/db"
"github.com/evcc-io/evcc/tariff"
)
var ErrIncomplete = errors.New("meter profile incomplete")
// energyProfile returns a 15min average meter profile in Wh. The profile
// is sorted by timestamp starting at 00:00. It is guaranteed to contain 96 15min values.
func energyProfile(entity entity, from time.Time) (*[96]float64, error) {
db, err := db.Instance.DB()
if err != nil {
return nil, err
}
// COALESCE guards against legacy rows with NULL energy
rows, err := db.Query(`SELECT min(ts) AS ts, COALESCE(avg(energy), 0) AS energy
FROM meters
WHERE meter = ? AND ts >= ? AND COALESCE(recovered, 0) = 0
GROUP BY strftime("%H:%M", ts, 'unixepoch', 'localtime')
ORDER BY strftime("%H:%M", ts, 'unixepoch', 'localtime') ASC`,
entity.Id, from.Unix(),
)
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
}