Collect household consumption metrics (#22526)
This commit is contained in:
parent
baeb602357
commit
b7015af551
3 changed files with 122 additions and 4 deletions
|
|
@ -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
73
core/metrics/db.go
Normal 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
|
||||
}
|
||||
48
core/site.go
48
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue