diff --git a/tariff/proxy_cache.go b/tariff/proxy_cache.go index bc6e2251d..3892a157e 100644 --- a/tariff/proxy_cache.go +++ b/tariff/proxy_cache.go @@ -11,18 +11,22 @@ import ( "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" - "github.com/jinzhu/now" ) +// defaultInterval is the update interval assumed if the tariff doesn't configure one +const defaultInterval = time.Hour + // cachingProxy wraps a tariff with caching type cachingProxy struct { mu sync.Mutex hash [32]byte - key string - ctx context.Context - typ string - config map[string]any + key string + ctx context.Context + typ string + config map[string]any + interval time.Duration + updated time.Time cached *cached tariff api.Tariff @@ -37,31 +41,41 @@ func NewCachedFromConfig(ctx context.Context, typ string, other map[string]any) tariffType = template } - p := &cachingProxy{ - ctx: ctx, - typ: typ, - config: other, - key: tariffType + "-" + cacheKey(typ, other), + cc := struct { + Interval time.Duration + Other map[string]any `mapstructure:",remain"` + }{ + Interval: defaultInterval, } - // check if we have cached data until end of tomorrow - data, err := p.cacheGet(untilEndOfTomorrow()) + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + p := &cachingProxy{ + ctx: ctx, + typ: typ, + config: other, + interval: cc.Interval, + key: tariffType + "-" + cacheKey(typ, other), + } + + // check if cached data is up to date + data, err := p.cacheGet() 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 + // if no cached data available, return error + if p.cached == nil { return nil, err } - // use cached data for the next 24 hours - data = atLeast2hrs + // use outdated cached data + data = p.cached } - // if instance creation was successful, cache it, otherwise use cached 24hrs of data + // if instance creation was successful, use it, otherwise use outdated cached data if err == nil { p.tariff = tariff } @@ -69,9 +83,7 @@ func NewCachedFromConfig(ctx context.Context, typ string, other map[string]any) 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(), - ) + log.DEBUG.Printf("using cache: %s (updated: %s)", p.key, data.Updated.Local()) } return p, nil @@ -92,7 +104,7 @@ func (p *cachingProxy) Rates() (api.Rates, error) { defer p.mu.Unlock() if p.tariff == nil { - if res, err := p.cacheGet(for24hrs()); err == nil { + if res, err := p.cacheGet(); err == nil { return slices.Clone(res.Rates), nil } @@ -117,7 +129,7 @@ func (p *cachingProxy) Type() api.TariffType { defer p.mu.Unlock() if p.tariff == nil { - if res, err := p.cacheGet(for24hrs()); err == nil { + if res, err := p.cacheGet(); err == nil { return res.Type } @@ -135,7 +147,8 @@ func (p *cachingProxy) dynamicTariff() bool { }, p.tariff.Type()) } -func (p *cachingProxy) cacheGet(until time.Time) (*cached, error) { +// cacheGet returns cached data if the update interval has not yet elapsed +func (p *cachingProxy) cacheGet() (*cached, error) { if p.cached == nil { res, err := cacheGet(p.key) if err != nil { @@ -145,31 +158,26 @@ func (p *cachingProxy) cacheGet(until time.Time) (*cached, error) { p.cached = res } - if !ratesValid(p.cached.Rates, until) { - return nil, errors.New("not enough rates") + if len(p.cached.Rates) == 0 { + return nil, errors.New("no rates") + } + + if d := time.Since(p.cached.Updated); d > p.interval { + return nil, fmt.Errorf("cache outdated: %v", d.Round(time.Second)) } return p.cached, nil } +// cachePut persists rates if changed or the update interval has elapsed func (p *cachingProxy) cachePut(typ api.TariffType, rates api.Rates) error { hash := sha256.Sum256(fmt.Append(nil, rates)) - if hash == p.hash { + if hash == p.hash && time.Since(p.updated) < p.interval { return nil } p.hash = hash + p.updated = time.Now() + 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) -} diff --git a/tariff/proxy_cache_helper.go b/tariff/proxy_cache_helper.go index b4fe00ed0..99aefa667 100644 --- a/tariff/proxy_cache_helper.go +++ b/tariff/proxy_cache_helper.go @@ -3,14 +3,16 @@ package tariff import ( "crypto/sha256" "fmt" + "time" "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"` + Type api.TariffType `json:"type"` + Rates api.Rates `json:"rates"` + Updated time.Time `json:"updated"` } func cacheKey(typ string, other map[string]any) string { @@ -19,8 +21,9 @@ func cacheKey(typ string, other map[string]any) string { func cachePut(key string, typ api.TariffType, rates api.Rates) error { return cache.Put(key, &cached{ - Type: typ, - Rates: rates, + Type: typ, + Rates: rates, + Updated: time.Now(), }) } diff --git a/tariff/proxy_cache_test.go b/tariff/proxy_cache_test.go new file mode 100644 index 000000000..5899e0951 --- /dev/null +++ b/tariff/proxy_cache_test.go @@ -0,0 +1,29 @@ +package tariff + +import ( + "testing" + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/server/db" + "github.com/stretchr/testify/require" +) + +func TestCacheInterval(t *testing.T) { + require.NoError(t, db.NewInstance("sqlite", ":memory:")) + + p := &cachingProxy{key: "test", interval: time.Hour} + require.NoError(t, p.cachePut(api.TariffTypeSolar, api.Rates{ + {Start: time.Now(), End: time.Now().Add(time.Hour), Value: 1}, + })) + + // within interval + res, err := p.cacheGet() + require.NoError(t, err) + require.Equal(t, api.TariffTypeSolar, res.Type) + + // interval elapsed + p.cached.Updated = time.Now().Add(-2 * time.Hour) + _, err = p.cacheGet() + require.Error(t, err) +} diff --git a/templates/definition/tariff/pvnode-v2.yaml b/templates/definition/tariff/pvnode-v2.yaml index 253e0db94..89a417064 100644 --- a/templates/definition/tariff/pvnode-v2.yaml +++ b/templates/definition/tariff/pvnode-v2.yaml @@ -36,7 +36,7 @@ params: default: 1 advanced: true - name: interval - default: 1h + default: 24h advanced: true render: |