evcc-io/core/metrics/db.go

134 lines
3.2 KiB
Go

package metrics
import (
"errors"
"slices"
"strings"
"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:meters_meter_ts"`
Timestamp int64 `json:"ts" gorm:"column:ts;uniqueIndex:meters_meter_ts"` // start of 15min slot
Entity entity `json:"-" gorm:"foreignkey:Meter;references:Id"`
Energy float64 `json:"energy" gorm:"column:energy"`
ReturnEnergy float64 `json:"returnEnergy" gorm:"column:return_energy"`
}
type entity struct {
Id int `gorm:"column:id;primarykey"`
Group string `gorm:"column:group;uniqueIndex:entities_group_name"`
Name string `gorm:"column:name;uniqueIndex:entities_group_name"`
Title string `gorm:"column:title"`
}
func init() {
db.Register(func(_ *gorm.DB) error {
return SetupSchema()
})
}
// SetupSchema is used for testing
func SetupSchema() error {
m := db.Instance.Migrator()
// entites: create entity first to make sure foreign keys for existing data work
if err := db.Instance.AutoMigrate(new(entity)); err != nil {
return err
}
// ensure home entity exists (reserves id=1 for legacy meter FK references)
if _, err := createEntity(Home, Home, Home); err != nil {
return err
}
// enable FK constraints only here to make sure entity for metric exists
if err := db.Instance.Exec("pragma foreign_keys(1)").Error; err != nil {
return err
}
// drop obsolete indexes
for _, idx := range []struct {
name string
obj any
}{
{"name_idx", new(entity)},
{"group_name", new(entity)},
{"meter_ts", new(meter)},
} {
if m.HasIndex(idx.obj, idx.name) {
if err := m.DropIndex(idx.obj, idx.name); err != nil {
return err
}
}
}
rename := func(from, to string) error {
if table := new(meter); m.HasColumn(table, from) && !m.HasColumn(table, to) {
return m.RenameColumn(table, from, to)
}
return nil
}
// meter: split energy direction
if err := rename("val", "import"); err != nil {
return err
}
// meter: split energy direction #2
if err := rename("pos", "import"); err != nil {
return err
}
if err := rename("neg", "export"); err != nil {
return err
}
// meter: rename to energy/return_energy
if err := rename("import", "energy"); err != nil {
return err
}
if err := rename("export", "return_energy"); err != nil {
return err
}
// meter: ts migration
if m.HasTable(new(meter)) {
types, err := m.ColumnTypes(new(meter))
if err != nil {
return err
}
tsIdx := slices.IndexFunc(types, func(typ gorm.ColumnType) bool {
return typ.Name() == "ts"
})
if tsIdx == -1 {
return errors.New("missing meters.ts")
}
if tsTyp, _ := types[tsIdx].ColumnType(); !strings.EqualFold(tsTyp, "INTEGER") {
db, err := db.Instance.DB()
if err != nil {
return err
}
if _, err := db.Exec(`UPDATE meters SET ts = unixepoch(ts)`); err != nil {
return err
}
}
}
return db.Instance.AutoMigrate(new(meter))
}
// persist stores 15min consumption in kWh
func persist(entity entity, ts time.Time, energy, returnEnergy float64) error {
return db.Instance.Create(&meter{
Meter: entity.Id,
Timestamp: ts.Truncate(tariff.SlotDuration).Unix(),
Energy: energy,
ReturnEnergy: returnEnergy,
}).Error
}