Skip to content

Commit b7015af

Browse files
authored
Collect household consumption metrics (#22526)
1 parent baeb602 commit b7015af

File tree

3 files changed

+122
-4
lines changed

3 files changed

+122
-4
lines changed

cmd/setup.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/evcc-io/evcc/core/circuit"
2323
"github.com/evcc-io/evcc/core/keys"
2424
"github.com/evcc-io/evcc/core/loadpoint"
25+
"github.com/evcc-io/evcc/core/metrics"
2526
"github.com/evcc-io/evcc/core/session"
2627
coresettings "github.com/evcc-io/evcc/core/settings"
2728
"github.com/evcc-io/evcc/hems"
@@ -579,6 +580,10 @@ func configureDatabase(conf globalconfig.DB) error {
579580
return err
580581
}
581582

583+
if err := metrics.Init(); err != nil {
584+
return err
585+
}
586+
582587
if err := settings.Init(); err != nil {
583588
return err
584589
}

core/metrics/db.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package metrics
2+
3+
import (
4+
"errors"
5+
"time"
6+
7+
"github.com/evcc-io/evcc/server/db"
8+
)
9+
10+
type meter struct {
11+
Meter int `json:"meter" gorm:"column:meter;uniqueIndex:meter_ts"`
12+
Timestamp time.Time `json:"ts" gorm:"column:ts;uniqueIndex:meter_ts"`
13+
Value float64 `json:"val" gorm:"column:val"`
14+
}
15+
16+
var ErrIncomplete = errors.New("meter profile incomplete")
17+
18+
func Init() error {
19+
return db.Instance.AutoMigrate(new(meter))
20+
}
21+
22+
// Persist stores 15min consumption in Wh
23+
func Persist(ts time.Time, value float64) error {
24+
return db.Instance.Save(meter{
25+
Meter: 1,
26+
Timestamp: ts.Truncate(15 * time.Minute),
27+
Value: value,
28+
}).Error
29+
}
30+
31+
// Profile returns a 15min average meter profile in Wh
32+
func Profile() (*[96]float64, error) {
33+
db, err := db.Instance.DB()
34+
if err != nil {
35+
return nil, err
36+
}
37+
38+
rows, err := db.Query(`SELECT min(ts) AS ts, avg(val) AS val
39+
FROM meters
40+
WHERE meter = ?
41+
GROUP BY strftime("HH:MM", ts)
42+
ORDER BY ts`, 1)
43+
if err != nil {
44+
return nil, err
45+
}
46+
defer rows.Close()
47+
48+
res := make([]float64, 0, 96)
49+
50+
for rows.Next() {
51+
var (
52+
ts time.Time
53+
val float64
54+
)
55+
if err := rows.Scan(&ts, &val); err != nil {
56+
return nil, err
57+
}
58+
59+
hour := ts.Hour()
60+
minute := ts.Minute() / 15
61+
if len(res) != hour*4+minute {
62+
return nil, ErrIncomplete
63+
}
64+
65+
res = append(res, val)
66+
}
67+
68+
if len(res) != 96 {
69+
return nil, ErrIncomplete
70+
}
71+
72+
return (*[96]float64)(res), nil
73+
}

core/site.go

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/evcc-io/evcc/core/coordinator"
1919
"github.com/evcc-io/evcc/core/keys"
2020
"github.com/evcc-io/evcc/core/loadpoint"
21+
"github.com/evcc-io/evcc/core/metrics"
2122
"github.com/evcc-io/evcc/core/planner"
2223
"github.com/evcc-io/evcc/core/prioritizer"
2324
"github.com/evcc-io/evcc/core/session"
@@ -100,6 +101,9 @@ type Site struct {
100101
fcstEnergy *meterEnergy
101102
pvEnergy map[string]*meterEnergy
102103

104+
householdEnergy *meterEnergy
105+
householdSlotStart time.Time
106+
103107
// cached state
104108
gridPower float64 // Grid power
105109
pvPower float64 // PV power
@@ -248,10 +252,11 @@ func (site *Site) Boot(log *util.Logger, loadpoints []*Loadpoint, tariffs *tarif
248252
// NewSite creates a Site with sane defaults
249253
func NewSite() *Site {
250254
site := &Site{
251-
log: util.NewLogger("site"),
252-
Voltage: 230, // V
253-
pvEnergy: make(map[string]*meterEnergy),
254-
fcstEnergy: &meterEnergy{clock: clock.New()},
255+
log: util.NewLogger("site"),
256+
Voltage: 230, // V
257+
pvEnergy: make(map[string]*meterEnergy),
258+
fcstEnergy: &meterEnergy{clock: clock.New()},
259+
householdEnergy: &meterEnergy{clock: clock.New()},
255260
}
256261

257262
return site
@@ -764,6 +769,39 @@ func (site *Site) updateMeters() error {
764769
return eg.Wait()
765770
}
766771

772+
func (site *Site) updateHouseholdConsumption(totalChargePower float64) {
773+
householdPower := site.gridPower + site.pvPower + site.batteryPower - totalChargePower
774+
if householdPower <= 0 {
775+
return
776+
}
777+
778+
site.householdEnergy.AddPower(householdPower)
779+
780+
now := site.householdEnergy.clock.Now()
781+
782+
if site.householdSlotStart.IsZero() {
783+
site.householdSlotStart = now
784+
return
785+
}
786+
787+
slotDuration := time.Minute
788+
slotStart := now.Truncate(slotDuration)
789+
790+
if slotStart.After(site.householdSlotStart) {
791+
// next slot has started
792+
if slotStart.Sub(site.householdSlotStart) >= slotDuration {
793+
// more or less full slot
794+
site.log.DEBUG.Printf("15min household consumption: %.0fWh", site.householdEnergy.Accumulated)
795+
if err := metrics.Persist(site.householdSlotStart, site.householdEnergy.Accumulated); err != nil {
796+
site.log.ERROR.Printf("persist household consumption: %v", err)
797+
}
798+
}
799+
800+
site.householdSlotStart = slotStart
801+
site.householdEnergy.Accumulated = 0
802+
}
803+
}
804+
767805
// sitePower returns
768806
// - the net power exported by the site minus a residual margin
769807
// (negative values mean grid: export, battery: charging
@@ -935,6 +973,8 @@ func (site *Site) update(lp updater) {
935973
site.log.ERROR.Println(err)
936974
}
937975

976+
site.updateHouseholdConsumption(totalChargePower)
977+
938978
site.stats.Update(site)
939979
}
940980

0 commit comments

Comments
 (0)