Tariff: base cache validity on update interval instead of rate coverage (#32168)
This commit is contained in:
parent
60f313cbfb
commit
be81052667
4 changed files with 85 additions and 45 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
29
tariff/proxy_cache_test.go
Normal file
29
tariff/proxy_cache_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -36,7 +36,7 @@ params:
|
|||
default: 1
|
||||
advanced: true
|
||||
- name: interval
|
||||
default: 1h
|
||||
default: 24h
|
||||
advanced: true
|
||||
|
||||
render: |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue