Tariffs: persist tariff values in 15min intervals (#31845)
Some checks failed
Some checks failed
This commit is contained in:
parent
d68e5e5809
commit
661af417a1
4 changed files with 130 additions and 0 deletions
42
core/metrics/tariffs.go
Normal file
42
core/metrics/tariffs.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/server/db"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type tariffValue struct {
|
||||
Timestamp int64 `gorm:"column:ts;uniqueIndex"` // 15min boundary
|
||||
Grid *float64 `gorm:"column:grid"`
|
||||
FeedIn *float64 `gorm:"column:feedin"`
|
||||
Co2 *float64 `gorm:"column:co2"`
|
||||
Temperature *float64 `gorm:"column:temperature"`
|
||||
}
|
||||
|
||||
func (tariffValue) TableName() string {
|
||||
return "tariffs"
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.Register(func(_ *gorm.DB) error {
|
||||
return db.Instance.AutoMigrate(new(tariffValue))
|
||||
})
|
||||
}
|
||||
|
||||
// PersistTariffs stores the tariff values at the given 15min boundary, nil values omitted
|
||||
func PersistTariffs(ts time.Time, grid, feedin, co2, temperature *float64) error {
|
||||
if grid == nil && feedin == nil && co2 == nil && temperature == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return db.Instance.Clauses(clause.OnConflict{DoNothing: true}).Create(&tariffValue{
|
||||
Timestamp: ts.Unix(),
|
||||
Grid: grid,
|
||||
FeedIn: feedin,
|
||||
Co2: co2,
|
||||
Temperature: temperature,
|
||||
}).Error
|
||||
}
|
||||
55
core/metrics/tariffs_test.go
Normal file
55
core/metrics/tariffs_test.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/server/db"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPersistTariffs(t *testing.T) {
|
||||
require.NoError(t, db.NewInstance("sqlite", ":memory:"))
|
||||
require.NoError(t, db.Instance.AutoMigrate(new(tariffValue)))
|
||||
|
||||
slot := time.Date(2026, 4, 15, 16, 15, 0, 0, time.UTC)
|
||||
grid, co2 := 0.3, 250.0
|
||||
|
||||
// nil values omitted
|
||||
require.NoError(t, PersistTariffs(slot, &grid, nil, &co2, nil))
|
||||
|
||||
var res tariffValue
|
||||
require.NoError(t, db.Instance.First(&res).Error)
|
||||
require.Equal(t, slot.Unix(), res.Timestamp)
|
||||
require.InDelta(t, 0.3, *res.Grid, 0.001)
|
||||
require.Nil(t, res.FeedIn)
|
||||
require.InDelta(t, 250, *res.Co2, 0.001)
|
||||
require.Nil(t, res.Temperature)
|
||||
|
||||
// duplicate slot ignored, first values kept
|
||||
other := 0.4
|
||||
require.NoError(t, PersistTariffs(slot, &other, nil, nil, nil))
|
||||
|
||||
var count int64
|
||||
require.NoError(t, db.Instance.Model(new(tariffValue)).Count(&count).Error)
|
||||
require.Equal(t, int64(1), count)
|
||||
|
||||
require.NoError(t, db.Instance.First(&res).Error)
|
||||
require.InDelta(t, 0.3, *res.Grid, 0.001)
|
||||
|
||||
// all nil: no row
|
||||
require.NoError(t, PersistTariffs(slot.Add(15*time.Minute), nil, nil, nil, nil))
|
||||
require.NoError(t, db.Instance.Model(new(tariffValue)).Count(&count).Error)
|
||||
require.Equal(t, int64(1), count)
|
||||
|
||||
// all values set: each column mapped independently
|
||||
next := slot.Add(30 * time.Minute)
|
||||
feedin, temp := 0.08, 21.5
|
||||
require.NoError(t, PersistTariffs(next, &grid, &feedin, &co2, &temp))
|
||||
|
||||
require.NoError(t, db.Instance.Where("ts = ?", next.Unix()).First(&res).Error)
|
||||
require.InDelta(t, 0.3, *res.Grid, 0.001)
|
||||
require.InDelta(t, 0.08, *res.FeedIn, 0.001)
|
||||
require.InDelta(t, 250, *res.Co2, 0.001)
|
||||
require.InDelta(t, 21.5, *res.Temperature, 0.001)
|
||||
}
|
||||
|
|
@ -94,6 +94,7 @@ type Site struct {
|
|||
stats *Stats // Stats
|
||||
|
||||
collectors map[string]*metrics.Collector // keyed by meter ref
|
||||
tariffSlot time.Time // last persisted tariff slot
|
||||
|
||||
// cached state
|
||||
gridPower float64 // Grid power
|
||||
|
|
|
|||
|
|
@ -121,6 +121,38 @@ func (site *Site) publishTariffs(greenShareHome float64, greenShareLoadpoints fl
|
|||
}
|
||||
|
||||
site.publish(keys.Forecast, util.NewSharder(keys.Forecast, fc))
|
||||
|
||||
site.persistTariffs()
|
||||
}
|
||||
|
||||
// persistTariffs stores tariff values once per 15min boundary. Like the meter
|
||||
// collectors it is driven by the update loop, skipping the partial boot slot.
|
||||
func (site *Site) persistTariffs() {
|
||||
slot := time.Now().Truncate(tariff.SlotDuration)
|
||||
|
||||
last := site.tariffSlot
|
||||
site.tariffSlot = slot
|
||||
|
||||
// skip repeat ticks within the slot and the partial boot slot
|
||||
if last.IsZero() || !slot.After(last) {
|
||||
return
|
||||
}
|
||||
|
||||
value := func(u api.TariffUsage) *float64 {
|
||||
if r, err := tariff.At(site.GetTariff(u), slot); err == nil {
|
||||
return &r.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := metrics.PersistTariffs(slot,
|
||||
value(api.TariffUsageGrid),
|
||||
value(api.TariffUsageFeedIn),
|
||||
value(api.TariffUsageCo2),
|
||||
value(api.TariffUsageTemperature),
|
||||
); err != nil {
|
||||
site.log.ERROR.Printf("persist tariffs: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (site *Site) solarDetails(solar api.Rates) solarDetails {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue