parent
0bbd39333c
commit
75dad99895
4 changed files with 215 additions and 1 deletions
|
|
@ -805,7 +805,7 @@ func tariffInstance(name string, conf config.Typed) (api.Tariff, error) {
|
|||
return nil, fmt.Errorf("cannot decode custom tariff '%s': %w", name, err)
|
||||
}
|
||||
|
||||
instance, err := tariff.NewFromConfig(ctx, conf.Type, props)
|
||||
instance, err := tariff.NewCachedFromConfig(ctx, conf.Type, props)
|
||||
if err != nil {
|
||||
if ce := new(util.ConfigError); errors.As(err, &ce) {
|
||||
return nil, err
|
||||
|
|
|
|||
167
tariff/proxy.go
Normal file
167
tariff/proxy.go
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
package tariff
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/jinzhu/now"
|
||||
)
|
||||
|
||||
// CachingProxy wraps a tariff with caching
|
||||
type CachingProxy struct {
|
||||
mu sync.Mutex
|
||||
|
||||
key string
|
||||
ctx context.Context
|
||||
typ string
|
||||
config map[string]any
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
p := &CachingProxy{
|
||||
ctx: ctx,
|
||||
typ: typ,
|
||||
config: other,
|
||||
key: tariffType + "-" + cacheKey(typ, other),
|
||||
}
|
||||
|
||||
// not cached yet, create instance
|
||||
if data, err := p.cacheGet(); err != nil {
|
||||
tariff, err := NewFromConfig(ctx, typ, other)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.tariff = tariff
|
||||
} else {
|
||||
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(); 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(); 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() (*cached, error) {
|
||||
res, err := cacheGet(p.key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// consider cache valid if it contains rates for 24 hours
|
||||
until := time.Now().Add(24 * time.Hour)
|
||||
|
||||
if !ratesValid(res.Rates, until) {
|
||||
return nil, errors.New("not enough rates")
|
||||
}
|
||||
|
||||
res.Rates = currentRates(res.Rates)
|
||||
if len(res.Rates) == 0 {
|
||||
return nil, errors.New("no current rates")
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (p *CachingProxy) cachePut(typ api.TariffType, rates api.Rates) error {
|
||||
return cachePut(p.key, typ, rates)
|
||||
}
|
||||
|
||||
func ratesValid(rr api.Rates, until time.Time) bool {
|
||||
if len(rr) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
rr.Sort()
|
||||
|
||||
return !rr[len(rr)-1].End.Before(until)
|
||||
}
|
||||
|
||||
func currentRates(rr api.Rates) api.Rates {
|
||||
res := make(api.Rates, 0, len(rr))
|
||||
now := now.With(time.Now()).BeginningOfHour()
|
||||
|
||||
for _, r := range rr {
|
||||
if !r.End.Before(now) {
|
||||
res = append(res, r)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
30
tariff/proxy_cache.go
Normal file
30
tariff/proxy_cache.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/settings"
|
||||
)
|
||||
|
||||
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([]byte(fmt.Sprintf("%s-%v", typ, other))))
|
||||
}
|
||||
|
||||
func cachePut(key string, typ api.TariffType, rates api.Rates) error {
|
||||
return settings.SetJson(key, &cached{
|
||||
Type: typ,
|
||||
Rates: rates,
|
||||
})
|
||||
}
|
||||
|
||||
func cacheGet(key string) (*cached, error) {
|
||||
var res cached
|
||||
return &res, settings.Json(key, &res)
|
||||
}
|
||||
17
tariff/proxy_error.go
Normal file
17
tariff/proxy_error.go
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package tariff
|
||||
|
||||
import "github.com/evcc-io/evcc/api"
|
||||
|
||||
type proxyError struct {
|
||||
error
|
||||
}
|
||||
|
||||
var _ api.Tariff = (*proxyError)(nil)
|
||||
|
||||
func (t *proxyError) Rates() (api.Rates, error) {
|
||||
return api.Rates{}, t.error
|
||||
}
|
||||
|
||||
func (t *proxyError) Type() api.TariffType {
|
||||
return 0 // unknown
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue