diff --git a/cmd/setup.go b/cmd/setup.go index 4d13266d2..e2a4b7d06 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -22,6 +22,7 @@ import ( "github.com/evcc-io/evcc/core/circuit" "github.com/evcc-io/evcc/core/keys" "github.com/evcc-io/evcc/core/loadpoint" + "github.com/evcc-io/evcc/core/metrics" "github.com/evcc-io/evcc/core/session" coresettings "github.com/evcc-io/evcc/core/settings" "github.com/evcc-io/evcc/hems" @@ -579,6 +580,10 @@ func configureDatabase(conf globalconfig.DB) error { return err } + if err := metrics.Init(); err != nil { + return err + } + if err := settings.Init(); err != nil { return err } diff --git a/core/metrics/db.go b/core/metrics/db.go new file mode 100644 index 000000000..fa269296c --- /dev/null +++ b/core/metrics/db.go @@ -0,0 +1,73 @@ +package metrics + +import ( + "errors" + "time" + + "github.com/evcc-io/evcc/server/db" +) + +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() error { + return db.Instance.AutoMigrate(new(meter)) +} + +// Persist stores 15min consumption in Wh +func Persist(ts time.Time, value float64) error { + return db.Instance.Save(meter{ + Meter: 1, + Timestamp: ts.Truncate(15 * time.Minute), + Value: value, + }).Error +} + +// Profile returns a 15min average meter profile in Wh +func Profile() (*[96]float64, error) { + db, err := db.Instance.DB() + if err != nil { + return nil, err + } + + rows, err := db.Query(`SELECT min(ts) AS ts, avg(val) AS val + FROM meters + WHERE meter = ? + GROUP BY strftime("HH:MM", ts) + ORDER BY ts`, 1) + if err != nil { + return nil, err + } + defer rows.Close() + + res := make([]float64, 0, 96) + + for rows.Next() { + var ( + ts time.Time + val float64 + ) + if err := rows.Scan(&ts, &val); err != nil { + return nil, err + } + + hour := ts.Hour() + minute := ts.Minute() / 15 + if len(res) != hour*4+minute { + return nil, ErrIncomplete + } + + res = append(res, val) + } + + if len(res) != 96 { + return nil, ErrIncomplete + } + + return (*[96]float64)(res), nil +} diff --git a/core/site.go b/core/site.go index ee668ab82..8b56c69da 100644 --- a/core/site.go +++ b/core/site.go @@ -18,6 +18,7 @@ import ( "github.com/evcc-io/evcc/core/coordinator" "github.com/evcc-io/evcc/core/keys" "github.com/evcc-io/evcc/core/loadpoint" + "github.com/evcc-io/evcc/core/metrics" "github.com/evcc-io/evcc/core/planner" "github.com/evcc-io/evcc/core/prioritizer" "github.com/evcc-io/evcc/core/session" @@ -100,6 +101,9 @@ type Site struct { fcstEnergy *meterEnergy pvEnergy map[string]*meterEnergy + householdEnergy *meterEnergy + householdSlotStart time.Time + // cached state gridPower float64 // Grid power pvPower float64 // PV power @@ -248,10 +252,11 @@ func (site *Site) Boot(log *util.Logger, loadpoints []*Loadpoint, tariffs *tarif // NewSite creates a Site with sane defaults func NewSite() *Site { site := &Site{ - log: util.NewLogger("site"), - Voltage: 230, // V - pvEnergy: make(map[string]*meterEnergy), - fcstEnergy: &meterEnergy{clock: clock.New()}, + log: util.NewLogger("site"), + Voltage: 230, // V + pvEnergy: make(map[string]*meterEnergy), + fcstEnergy: &meterEnergy{clock: clock.New()}, + householdEnergy: &meterEnergy{clock: clock.New()}, } return site @@ -764,6 +769,39 @@ func (site *Site) updateMeters() error { return eg.Wait() } +func (site *Site) updateHouseholdConsumption(totalChargePower float64) { + householdPower := site.gridPower + site.pvPower + site.batteryPower - totalChargePower + if householdPower <= 0 { + return + } + + site.householdEnergy.AddPower(householdPower) + + now := site.householdEnergy.clock.Now() + + if site.householdSlotStart.IsZero() { + site.householdSlotStart = now + return + } + + slotDuration := time.Minute + slotStart := now.Truncate(slotDuration) + + if slotStart.After(site.householdSlotStart) { + // next slot has started + if slotStart.Sub(site.householdSlotStart) >= slotDuration { + // more or less full slot + site.log.DEBUG.Printf("15min household consumption: %.0fWh", site.householdEnergy.Accumulated) + if err := metrics.Persist(site.householdSlotStart, site.householdEnergy.Accumulated); err != nil { + site.log.ERROR.Printf("persist household consumption: %v", err) + } + } + + site.householdSlotStart = slotStart + site.householdEnergy.Accumulated = 0 + } +} + // sitePower returns // - the net power exported by the site minus a residual margin // (negative values mean grid: export, battery: charging @@ -935,6 +973,8 @@ func (site *Site) update(lp updater) { site.log.ERROR.Println(err) } + site.updateHouseholdConsumption(totalChargePower) + site.stats.Update(site) }