Tariffs: add optional hourly averaging (#24829)
This commit is contained in:
parent
642a6594f3
commit
4a64c65778
14 changed files with 361 additions and 201 deletions
|
|
@ -8,6 +8,7 @@ const (
|
||||||
CoarseCurrent // charger
|
CoarseCurrent // charger
|
||||||
IntegratedDevice // charger
|
IntegratedDevice // charger
|
||||||
Heating // charger
|
Heating // charger
|
||||||
|
Average // tariff
|
||||||
Cacheable // tariff
|
Cacheable // tariff
|
||||||
Offline // vehicle
|
Offline // vehicle
|
||||||
Retryable // vehicle
|
Retryable // vehicle
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const _FeatureName = "CoarseCurrentIntegratedDeviceHeatingCacheableOfflineRetryableStreamingWelcomeCharge"
|
const _FeatureName = "CoarseCurrentIntegratedDeviceHeatingAverageCacheableOfflineRetryableStreamingWelcomeCharge"
|
||||||
|
|
||||||
var _FeatureIndex = [...]uint8{0, 13, 29, 36, 45, 52, 61, 70, 83}
|
var _FeatureIndex = [...]uint8{0, 13, 29, 36, 43, 52, 59, 68, 77, 90}
|
||||||
|
|
||||||
const _FeatureLowerName = "coarsecurrentintegrateddeviceheatingcacheableofflineretryablestreamingwelcomecharge"
|
const _FeatureLowerName = "coarsecurrentintegrateddeviceheatingaveragecacheableofflineretryablestreamingwelcomecharge"
|
||||||
|
|
||||||
func (i Feature) String() string {
|
func (i Feature) String() string {
|
||||||
i -= 1
|
i -= 1
|
||||||
|
|
@ -28,14 +28,15 @@ func _FeatureNoOp() {
|
||||||
_ = x[CoarseCurrent-(1)]
|
_ = x[CoarseCurrent-(1)]
|
||||||
_ = x[IntegratedDevice-(2)]
|
_ = x[IntegratedDevice-(2)]
|
||||||
_ = x[Heating-(3)]
|
_ = x[Heating-(3)]
|
||||||
_ = x[Cacheable-(4)]
|
_ = x[Average-(4)]
|
||||||
_ = x[Offline-(5)]
|
_ = x[Cacheable-(5)]
|
||||||
_ = x[Retryable-(6)]
|
_ = x[Offline-(6)]
|
||||||
_ = x[Streaming-(7)]
|
_ = x[Retryable-(7)]
|
||||||
_ = x[WelcomeCharge-(8)]
|
_ = x[Streaming-(8)]
|
||||||
|
_ = x[WelcomeCharge-(9)]
|
||||||
}
|
}
|
||||||
|
|
||||||
var _FeatureValues = []Feature{CoarseCurrent, IntegratedDevice, Heating, Cacheable, Offline, Retryable, Streaming, WelcomeCharge}
|
var _FeatureValues = []Feature{CoarseCurrent, IntegratedDevice, Heating, Average, Cacheable, Offline, Retryable, Streaming, WelcomeCharge}
|
||||||
|
|
||||||
var _FeatureNameToValueMap = map[string]Feature{
|
var _FeatureNameToValueMap = map[string]Feature{
|
||||||
_FeatureName[0:13]: CoarseCurrent,
|
_FeatureName[0:13]: CoarseCurrent,
|
||||||
|
|
@ -44,27 +45,30 @@ var _FeatureNameToValueMap = map[string]Feature{
|
||||||
_FeatureLowerName[13:29]: IntegratedDevice,
|
_FeatureLowerName[13:29]: IntegratedDevice,
|
||||||
_FeatureName[29:36]: Heating,
|
_FeatureName[29:36]: Heating,
|
||||||
_FeatureLowerName[29:36]: Heating,
|
_FeatureLowerName[29:36]: Heating,
|
||||||
_FeatureName[36:45]: Cacheable,
|
_FeatureName[36:43]: Average,
|
||||||
_FeatureLowerName[36:45]: Cacheable,
|
_FeatureLowerName[36:43]: Average,
|
||||||
_FeatureName[45:52]: Offline,
|
_FeatureName[43:52]: Cacheable,
|
||||||
_FeatureLowerName[45:52]: Offline,
|
_FeatureLowerName[43:52]: Cacheable,
|
||||||
_FeatureName[52:61]: Retryable,
|
_FeatureName[52:59]: Offline,
|
||||||
_FeatureLowerName[52:61]: Retryable,
|
_FeatureLowerName[52:59]: Offline,
|
||||||
_FeatureName[61:70]: Streaming,
|
_FeatureName[59:68]: Retryable,
|
||||||
_FeatureLowerName[61:70]: Streaming,
|
_FeatureLowerName[59:68]: Retryable,
|
||||||
_FeatureName[70:83]: WelcomeCharge,
|
_FeatureName[68:77]: Streaming,
|
||||||
_FeatureLowerName[70:83]: WelcomeCharge,
|
_FeatureLowerName[68:77]: Streaming,
|
||||||
|
_FeatureName[77:90]: WelcomeCharge,
|
||||||
|
_FeatureLowerName[77:90]: WelcomeCharge,
|
||||||
}
|
}
|
||||||
|
|
||||||
var _FeatureNames = []string{
|
var _FeatureNames = []string{
|
||||||
_FeatureName[0:13],
|
_FeatureName[0:13],
|
||||||
_FeatureName[13:29],
|
_FeatureName[13:29],
|
||||||
_FeatureName[29:36],
|
_FeatureName[29:36],
|
||||||
_FeatureName[36:45],
|
_FeatureName[36:43],
|
||||||
_FeatureName[45:52],
|
_FeatureName[43:52],
|
||||||
_FeatureName[52:61],
|
_FeatureName[52:59],
|
||||||
_FeatureName[61:70],
|
_FeatureName[59:68],
|
||||||
_FeatureName[70:83],
|
_FeatureName[68:77],
|
||||||
|
_FeatureName[77:90],
|
||||||
}
|
}
|
||||||
|
|
||||||
// FeatureString retrieves an enum value from the enum constants string name.
|
// FeatureString retrieves an enum value from the enum constants string name.
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,14 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/evcc-io/evcc/api"
|
||||||
"github.com/evcc-io/evcc/plugin/golang/stdlib"
|
"github.com/evcc-io/evcc/plugin/golang/stdlib"
|
||||||
"github.com/traefik/yaegi/interp"
|
"github.com/traefik/yaegi/interp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type embed struct {
|
type embed struct {
|
||||||
|
Features_ []api.Feature `mapstructure:"features"`
|
||||||
|
|
||||||
Charges float64 `mapstructure:"charges"`
|
Charges float64 `mapstructure:"charges"`
|
||||||
Tax float64 `mapstructure:"tax"`
|
Tax float64 `mapstructure:"tax"`
|
||||||
Formula string `mapstructure:"formula"`
|
Formula string `mapstructure:"formula"`
|
||||||
|
|
@ -70,3 +73,9 @@ func (t *embed) totalPrice(price float64, ts time.Time) float64 {
|
||||||
}
|
}
|
||||||
return (price + t.Charges) * (1 + t.Tax)
|
return (price + t.Charges) * (1 + t.Tax)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ api.FeatureDescriber = (*embed)(nil)
|
||||||
|
|
||||||
|
func (t *embed) Features() []api.Feature {
|
||||||
|
return t.Features_
|
||||||
|
}
|
||||||
|
|
|
||||||
175
tariff/proxy.go
175
tariff/proxy.go
|
|
@ -2,43 +2,14 @@ package tariff
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"slices"
|
"slices"
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/evcc-io/evcc/api"
|
"github.com/evcc-io/evcc/api"
|
||||||
"github.com/evcc-io/evcc/util"
|
"github.com/evcc-io/evcc/util"
|
||||||
"github.com/jinzhu/now"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CachingProxy wraps a tariff with caching
|
// NewProxyFromConfig creates a tariff proxy supporting average or caching
|
||||||
type CachingProxy struct {
|
func NewProxyFromConfig(ctx context.Context, typ string, other map[string]any) (api.Tariff, error) {
|
||||||
mu sync.Mutex
|
|
||||||
hash [32]byte
|
|
||||||
|
|
||||||
key string
|
|
||||||
ctx context.Context
|
|
||||||
typ string
|
|
||||||
config map[string]any
|
|
||||||
|
|
||||||
cached *cached
|
|
||||||
tariff api.Tariff
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ api.Tariff = (*CachingProxy)(nil)
|
|
||||||
|
|
||||||
// NewCachedFromConfig creates a proxy that controls tariff instantiation and caching
|
|
||||||
func NewCachedFromConfig(ctx context.Context, typ string, other map[string]any) (api.Tariff, error) {
|
|
||||||
tariffType := typ
|
|
||||||
if typ == "template" {
|
|
||||||
if template, ok := other["template"].(string); ok {
|
|
||||||
tariffType = template
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var embed struct {
|
var embed struct {
|
||||||
Features []api.Feature `mapstructure:"features"`
|
Features []api.Feature `mapstructure:"features"`
|
||||||
Other map[string]any `mapstructure:",remain"`
|
Other map[string]any `mapstructure:",remain"`
|
||||||
|
|
@ -48,143 +19,25 @@ func NewCachedFromConfig(ctx context.Context, typ string, other map[string]any)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !slices.Contains(embed.Features, api.Cacheable) {
|
if slices.Contains(embed.Features, api.Average) {
|
||||||
return NewFromConfig(ctx, typ, embed.Other)
|
embed.Other["features"] = sliceMinusElement(embed.Features, api.Average)
|
||||||
}
|
t, err := NewFromConfig(ctx, typ, embed.Other)
|
||||||
|
|
||||||
p := &CachingProxy{
|
|
||||||
ctx: ctx,
|
|
||||||
typ: typ,
|
|
||||||
config: embed.Other,
|
|
||||||
key: tariffType + "-" + cacheKey(typ, other),
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if we have cached data until end of tomorrow
|
|
||||||
data, err := p.cacheGet(untilEndOfTomorrow())
|
|
||||||
if err != nil {
|
|
||||||
// attempt to create a new instance
|
|
||||||
tariff, err := NewFromConfig(ctx, typ, embed.Other)
|
|
||||||
if err != nil {
|
|
||||||
// check if we have at least data for the next 24 hours
|
|
||||||
atLeast2hrs, err2 := p.cacheGet(for24hrs())
|
|
||||||
if err2 != nil {
|
|
||||||
// if not available, return error
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// use cached data for the next 24 hours
|
|
||||||
data = atLeast2hrs
|
|
||||||
}
|
|
||||||
|
|
||||||
// if instance creation was successful, cache it, otherwise use cached 24hrs of data
|
|
||||||
if err == nil {
|
|
||||||
p.tariff = tariff
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if data != nil {
|
|
||||||
log := util.NewLogger("tariff")
|
|
||||||
log.DEBUG.Printf("using cache: %s (start: %s, end: %s)", p.key,
|
|
||||||
data.Rates[0].Start.Local(), data.Rates[len(data.Rates)-1].End.Local(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *CachingProxy) createInstance() {
|
|
||||||
t, err := NewFromConfig(p.ctx, p.typ, p.config)
|
|
||||||
if err != nil {
|
|
||||||
t = &proxyError{err}
|
|
||||||
}
|
|
||||||
|
|
||||||
p.tariff = t
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rates returns cached data until underlying tariff is created, then delegates to tariff
|
|
||||||
func (p *CachingProxy) Rates() (api.Rates, error) {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
if p.tariff == nil {
|
|
||||||
if res, err := p.cacheGet(for24hrs()); err == nil {
|
|
||||||
return res.Rates, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
p.createInstance()
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := p.tariff.Rates()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.dynamicTariff() {
|
|
||||||
err = p.cachePut(p.tariff.Type(), res)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type returns the tariff type
|
|
||||||
func (p *CachingProxy) Type() api.TariffType {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
if p.tariff == nil {
|
|
||||||
if res, err := p.cacheGet(for24hrs()); err == nil {
|
|
||||||
return res.Type
|
|
||||||
}
|
|
||||||
|
|
||||||
p.createInstance()
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.tariff.Type()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *CachingProxy) dynamicTariff() bool {
|
|
||||||
return slices.Contains([]api.TariffType{
|
|
||||||
api.TariffTypePriceForecast,
|
|
||||||
api.TariffTypeCo2,
|
|
||||||
api.TariffTypeSolar,
|
|
||||||
}, p.tariff.Type())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *CachingProxy) cacheGet(until time.Time) (*cached, error) {
|
|
||||||
if p.cached == nil {
|
|
||||||
res, err := cacheGet(p.key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return NewAverageProxy(t)
|
||||||
p.cached = res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ratesValid(p.cached.Rates, until) {
|
if slices.Contains(embed.Features, api.Cacheable) {
|
||||||
return nil, errors.New("not enough rates")
|
embed.Other["features"] = sliceMinusElement(embed.Features, api.Cacheable)
|
||||||
|
return NewCachedFromConfig(ctx, typ, embed.Other)
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.cached, nil
|
return NewFromConfig(ctx, typ, other)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *CachingProxy) cachePut(typ api.TariffType, rates api.Rates) error {
|
func sliceMinusElement[T comparable](s []T, el T) []T {
|
||||||
hash := sha256.Sum256(fmt.Append(nil, rates))
|
return slices.DeleteFunc(s, func(f T) bool {
|
||||||
if hash == p.hash {
|
return el == f
|
||||||
return nil
|
})
|
||||||
}
|
|
||||||
|
|
||||||
p.hash = hash
|
|
||||||
return cachePut(p.key, typ, rates)
|
|
||||||
}
|
|
||||||
|
|
||||||
func for24hrs() time.Time {
|
|
||||||
return time.Now().Add(24 * time.Hour)
|
|
||||||
}
|
|
||||||
|
|
||||||
func untilEndOfTomorrow() time.Time {
|
|
||||||
return now.BeginningOfDay().AddDate(0, 0, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ratesValid(rr api.Rates, until time.Time) bool {
|
|
||||||
return len(rr) > 0 && !rr[len(rr)-1].End.Before(until)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
66
tariff/proxy_average.go
Normal file
66
tariff/proxy_average.go
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
package tariff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/evcc-io/evcc/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// average wraps a tariff with averaging
|
||||||
|
type average struct {
|
||||||
|
average time.Duration
|
||||||
|
api.Tariff
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAverageProxy creates a proxy that tariff averaging
|
||||||
|
func NewAverageProxy(t api.Tariff) (api.Tariff, error) {
|
||||||
|
return &average{
|
||||||
|
average: time.Hour,
|
||||||
|
Tariff: t,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *average) Rates() (api.Rates, error) {
|
||||||
|
rates, err := t.Tariff.Rates()
|
||||||
|
if len(rates) == 0 || err != nil {
|
||||||
|
return rates, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return averageSlots(rates, t.average), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// averageSlots averages 15-minute slots by period
|
||||||
|
func averageSlots(rates api.Rates, average time.Duration) api.Rates {
|
||||||
|
if len(rates) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// accumulate sums and counts per period
|
||||||
|
avgs := make(map[time.Time]*struct {
|
||||||
|
sum float64
|
||||||
|
cnt int
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, r := range rates {
|
||||||
|
ts := r.Start.Truncate(average)
|
||||||
|
avg, ok := avgs[ts]
|
||||||
|
if !ok {
|
||||||
|
avg = new(struct {
|
||||||
|
sum float64
|
||||||
|
cnt int
|
||||||
|
})
|
||||||
|
avgs[ts] = avg
|
||||||
|
}
|
||||||
|
avg.sum += r.Value
|
||||||
|
avg.cnt++
|
||||||
|
}
|
||||||
|
|
||||||
|
res := slices.Clone(rates)
|
||||||
|
for i, r := range res {
|
||||||
|
avg := avgs[r.Start.Truncate(average)]
|
||||||
|
res[i].Value = avg.sum / float64(avg.cnt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
35
tariff/proxy_average_test.go
Normal file
35
tariff/proxy_average_test.go
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
package tariff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/benbjohnson/clock"
|
||||||
|
"github.com/evcc-io/evcc/api"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAverage(t *testing.T) {
|
||||||
|
clock := clock.NewMock()
|
||||||
|
clock.Add(30 * time.Minute)
|
||||||
|
|
||||||
|
var rr api.Rates
|
||||||
|
for i := range 5 {
|
||||||
|
rr = append(rr, api.Rate{
|
||||||
|
Start: clock.Now(),
|
||||||
|
End: clock.Now().Add(SlotDuration),
|
||||||
|
Value: float64(i + 1),
|
||||||
|
})
|
||||||
|
clock.Add(SlotDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
res := averageSlots(rr, time.Hour)
|
||||||
|
|
||||||
|
rr[0].Value = 1.5
|
||||||
|
rr[1].Value = 1.5
|
||||||
|
rr[2].Value = 4.0
|
||||||
|
rr[3].Value = 4.0
|
||||||
|
rr[4].Value = 4.0
|
||||||
|
|
||||||
|
require.Equal(t, rr, res)
|
||||||
|
}
|
||||||
|
|
@ -1,30 +1,177 @@
|
||||||
package tariff
|
package tariff
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/evcc-io/evcc/api"
|
"github.com/evcc-io/evcc/api"
|
||||||
"github.com/evcc-io/evcc/server/db/cache"
|
"github.com/evcc-io/evcc/util"
|
||||||
|
"github.com/jinzhu/now"
|
||||||
)
|
)
|
||||||
|
|
||||||
type cached struct {
|
// cachingProxy wraps a tariff with caching
|
||||||
Type api.TariffType `json:"type"`
|
type cachingProxy struct {
|
||||||
Rates api.Rates `json:"rates"`
|
mu sync.Mutex
|
||||||
|
hash [32]byte
|
||||||
|
|
||||||
|
key string
|
||||||
|
ctx context.Context
|
||||||
|
typ string
|
||||||
|
config map[string]any
|
||||||
|
|
||||||
|
cached *cached
|
||||||
|
tariff api.Tariff
|
||||||
}
|
}
|
||||||
|
|
||||||
func cacheKey(typ string, other map[string]any) string {
|
var _ api.Tariff = (*cachingProxy)(nil)
|
||||||
return fmt.Sprintf("%x", sha256.Sum256(fmt.Appendf(nil, "%s-%v", typ, other)))
|
|
||||||
|
// NewCachedFromConfig creates a proxy that controls tariff instantiation and caching
|
||||||
|
func NewCachedFromConfig(ctx context.Context, typ string, other map[string]any) (api.Tariff, error) {
|
||||||
|
tariffType := typ
|
||||||
|
if typ == "template" {
|
||||||
|
if template, ok := other["template"].(string); ok {
|
||||||
|
tariffType = template
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p := &cachingProxy{
|
||||||
|
ctx: ctx,
|
||||||
|
typ: typ,
|
||||||
|
config: other,
|
||||||
|
key: tariffType + "-" + cacheKey(typ, other),
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we have cached data until end of tomorrow
|
||||||
|
data, err := p.cacheGet(untilEndOfTomorrow())
|
||||||
|
if err != nil {
|
||||||
|
// attempt to create a new instance
|
||||||
|
tariff, err := NewFromConfig(ctx, typ, other)
|
||||||
|
if err != nil {
|
||||||
|
// check if we have at least data for the next 24 hours
|
||||||
|
atLeast2hrs, err2 := p.cacheGet(for24hrs())
|
||||||
|
if err2 != nil {
|
||||||
|
// if not available, return error
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// use cached data for the next 24 hours
|
||||||
|
data = atLeast2hrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// if instance creation was successful, cache it, otherwise use cached 24hrs of data
|
||||||
|
if err == nil {
|
||||||
|
p.tariff = tariff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if data != nil {
|
||||||
|
log := util.NewLogger("tariff")
|
||||||
|
log.DEBUG.Printf("using cache: %s (start: %s, end: %s)", p.key,
|
||||||
|
data.Rates[0].Start.Local(), data.Rates[len(data.Rates)-1].End.Local(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cachePut(key string, typ api.TariffType, rates api.Rates) error {
|
func (p *cachingProxy) createInstance() {
|
||||||
return cache.Put(key, &cached{
|
t, err := NewFromConfig(p.ctx, p.typ, p.config)
|
||||||
Type: typ,
|
if err != nil {
|
||||||
Rates: rates,
|
t = &proxyError{err}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
p.tariff = t
|
||||||
}
|
}
|
||||||
|
|
||||||
func cacheGet(key string) (*cached, error) {
|
// Rates returns cached data until underlying tariff is created, then delegates to tariff
|
||||||
var res cached
|
func (p *cachingProxy) Rates() (api.Rates, error) {
|
||||||
return &res, cache.Get(key, &res)
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
if p.tariff == nil {
|
||||||
|
if res, err := p.cacheGet(for24hrs()); err == nil {
|
||||||
|
return res.Rates, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p.createInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := p.tariff.Rates()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.dynamicTariff() {
|
||||||
|
err = p.cachePut(p.tariff.Type(), res)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the tariff type
|
||||||
|
func (p *cachingProxy) Type() api.TariffType {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
if p.tariff == nil {
|
||||||
|
if res, err := p.cacheGet(for24hrs()); err == nil {
|
||||||
|
return res.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
p.createInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.tariff.Type()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *cachingProxy) dynamicTariff() bool {
|
||||||
|
return slices.Contains([]api.TariffType{
|
||||||
|
api.TariffTypePriceForecast,
|
||||||
|
api.TariffTypeCo2,
|
||||||
|
api.TariffTypeSolar,
|
||||||
|
}, p.tariff.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *cachingProxy) cacheGet(until time.Time) (*cached, error) {
|
||||||
|
if p.cached == nil {
|
||||||
|
res, err := cacheGet(p.key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.cached = res
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ratesValid(p.cached.Rates, until) {
|
||||||
|
return nil, errors.New("not enough rates")
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.cached, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *cachingProxy) cachePut(typ api.TariffType, rates api.Rates) error {
|
||||||
|
hash := sha256.Sum256(fmt.Append(nil, rates))
|
||||||
|
if hash == p.hash {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p.hash = hash
|
||||||
|
return cachePut(p.key, typ, rates)
|
||||||
|
}
|
||||||
|
|
||||||
|
func for24hrs() time.Time {
|
||||||
|
return time.Now().Add(24 * time.Hour)
|
||||||
|
}
|
||||||
|
|
||||||
|
func untilEndOfTomorrow() time.Time {
|
||||||
|
return now.BeginningOfDay().AddDate(0, 0, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ratesValid(rr api.Rates, until time.Time) bool {
|
||||||
|
return len(rr) > 0 && !rr[len(rr)-1].End.Before(until)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
30
tariff/proxy_cache_helper.go
Normal file
30
tariff/proxy_cache_helper.go
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
package tariff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/evcc-io/evcc/api"
|
||||||
|
"github.com/evcc-io/evcc/server/db/cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cached struct {
|
||||||
|
Type api.TariffType `json:"type"`
|
||||||
|
Rates api.Rates `json:"rates"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func cacheKey(typ string, other map[string]any) string {
|
||||||
|
return fmt.Sprintf("%x", sha256.Sum256(fmt.Appendf(nil, "%s-%v", typ, other)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func cachePut(key string, typ api.TariffType, rates api.Rates) error {
|
||||||
|
return cache.Put(key, &cached{
|
||||||
|
Type: typ,
|
||||||
|
Rates: rates,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func cacheGet(key string) (*cached, error) {
|
||||||
|
var res cached
|
||||||
|
return &res, cache.Get(key, &res)
|
||||||
|
}
|
||||||
|
|
@ -17,5 +17,5 @@ func NewTariffFromTemplateConfig(ctx context.Context, other map[string]interface
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewCachedFromConfig(ctx, instance.Type, instance.Other)
|
return NewProxyFromConfig(ctx, instance.Type, instance.Other)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,10 @@ params:
|
||||||
de: "siehe https://transparency.entsoe.eu/content/static_content/Static%20content/web%20api/Guide.html#_areas"
|
de: "siehe https://transparency.entsoe.eu/content/static_content/Static%20content/web%20api/Guide.html#_areas"
|
||||||
en: "see https://transparency.entsoe.eu/content/static_content/Static%20content/web%20api/Guide.html#_areas"
|
en: "see https://transparency.entsoe.eu/content/static_content/Static%20content/web%20api/Guide.html#_areas"
|
||||||
- preset: tariff-base
|
- preset: tariff-base
|
||||||
|
- preset: tariff-features
|
||||||
render: |
|
render: |
|
||||||
type: entsoe
|
type: entsoe
|
||||||
securitytoken: {{ .securitytoken }}
|
securitytoken: {{ .securitytoken }}
|
||||||
domain: {{ .domain }}
|
domain: {{ .domain }}
|
||||||
{{ include "tariff-base" . }}
|
{{ include "tariff-base" . }}
|
||||||
|
{{ include "tariff-features" . }}
|
||||||
|
|
|
||||||
|
|
@ -515,6 +515,13 @@ presets:
|
||||||
de: Individuelle Formel zur Berechnung des Preises
|
de: Individuelle Formel zur Berechnung des Preises
|
||||||
en: Individual formula for calculating the price
|
en: Individual formula for calculating the price
|
||||||
example: "math.Max((price + charges) * (1 + tax), 0.0)"
|
example: "math.Max((price + charges) * (1 + tax), 0.0)"
|
||||||
|
tariff-features:
|
||||||
|
- name: average
|
||||||
|
type: bool
|
||||||
|
description:
|
||||||
|
en: Average by hour
|
||||||
|
de: Stündliche Durchschnittskosten verwenden
|
||||||
|
advanced: true
|
||||||
forecast-base:
|
forecast-base:
|
||||||
- name: lat
|
- name: lat
|
||||||
required: true
|
required: true
|
||||||
|
|
@ -565,6 +572,7 @@ presets:
|
||||||
en: Shows °C instead of %
|
en: Shows °C instead of %
|
||||||
- name: icon
|
- name: icon
|
||||||
advanced: true
|
advanced: true
|
||||||
|
|
||||||
ocpp:
|
ocpp:
|
||||||
- name: stationid
|
- name: stationid
|
||||||
type: string
|
type: string
|
||||||
|
|
|
||||||
5
util/templates/includes/tariff-features.tpl
Normal file
5
util/templates/includes/tariff-features.tpl
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{{ define "tariff-features" }}
|
||||||
|
{{- if eq .average "true" }}
|
||||||
|
features: ["average"]
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{{ define "vehicle-features" }}
|
{{ define "vehicle-features" }}
|
||||||
{{- if or (eq .coarsecurrent "true") (eq .welcomecharge "true") }}
|
{{- if or (eq .coarsecurrent "true") (eq .welcomecharge "true") (eq .streaming "true") }}
|
||||||
features:
|
features:
|
||||||
{{- if eq .coarsecurrent "true" }}
|
{{- if eq .coarsecurrent "true" }}
|
||||||
- coarsecurrent
|
- coarsecurrent
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue