Collect household consumption metrics (#22526)

This commit is contained in:
andig 2025-07-25 14:59:08 +02:00 • committed by GitHub
parent baeb602357
commit b7015af551
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 122 additions and 4 deletions

View file

@ -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
}

73
core/metrics/db.go Normal file
View file

@ -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
}

View file

@ -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)
}