diff --git a/tariff/tariff.go b/tariff/tariff.go index 0333b69ec9..7dd64fe56e 100644 --- a/tariff/tariff.go +++ b/tariff/tariff.go @@ -1,9 +1,13 @@ package tariff import ( + "encoding/json" "fmt" + "slices" + "sync" "time" + "github.com/cenkalti/backoff/v4" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/provider" "github.com/evcc-io/evcc/util" @@ -12,6 +16,8 @@ import ( type Tariff struct { *embed + log *util.Logger + data *util.Monitor[api.Rates] priceG func() (float64, error) } @@ -23,29 +29,92 @@ func init() { func NewConfigurableFromConfig(other map[string]interface{}) (api.Tariff, error) { var cc struct { - embed `mapstructure:",squash"` - Price provider.Config + embed `mapstructure:",squash"` + Price *provider.Config + Forecast *provider.Config } if err := util.DecodeOther(other, &cc); err != nil { return nil, err } - priceG, err := provider.NewFloatGetterFromConfig(cc.Price) - if err != nil { - return nil, fmt.Errorf("price: %w", err) + if (cc.Price != nil) == (cc.Forecast != nil) { + return nil, fmt.Errorf("must have either price or forecast") + } + + var ( + err error + priceG func() (float64, error) + forecastG func() (string, error) + ) + + if cc.Price != nil { + priceG, err = provider.NewFloatGetterFromConfig(*cc.Price) + if err != nil { + return nil, fmt.Errorf("price: %w", err) + } + } + + if cc.Forecast != nil { + forecastG, err = provider.NewStringGetterFromConfig(*cc.Forecast) + if err != nil { + return nil, fmt.Errorf("forecast: %w", err) + } } t := &Tariff{ + log: util.NewLogger("tariff"), embed: &cc.embed, priceG: priceG, + data: util.NewMonitor[api.Rates](2 * time.Hour), } - return t, nil + if forecastG != nil { + done := make(chan error) + go t.run(forecastG, done) + err = <-done + } + + return t, err } -// Rates implements the api.Tariff interface -func (t *Tariff) Rates() (api.Rates, error) { +func (t *Tariff) run(forecastG func() (string, error), done chan error) { + var once sync.Once + bo := newBackoff() + + tick := time.NewTicker(time.Hour) + for ; true; <-tick.C { + var data api.Rates + if err := backoff.Retry(func() error { + s, err := forecastG() + if err != nil { + return backoffPermanentError(err) + } + + return json.Unmarshal([]byte(s), &data) + }, bo); err != nil { + once.Do(func() { done <- err }) + + t.log.ERROR.Println(err) + continue + } + + data.Sort() + + t.data.Set(data) + once.Do(func() { close(done) }) + } +} + +func (t *Tariff) forecastRates() (api.Rates, error) { + var res api.Rates + err := t.data.GetFunc(func(val api.Rates) { + res = slices.Clone(val) + }) + return res, err +} + +func (t *Tariff) priceRates() (api.Rates, error) { price, err := t.priceG() if err != nil { return nil, err @@ -66,7 +135,19 @@ func (t *Tariff) Rates() (api.Rates, error) { return res, nil } +// Rates implements the api.Tariff interface +func (t *Tariff) Rates() (api.Rates, error) { + if t.priceG != nil { + return t.priceRates() + } + + return t.forecastRates() +} + // Type implements the api.Tariff interface func (t *Tariff) Type() api.TariffType { - return api.TariffTypePriceDynamic + if t.priceG != nil { + return api.TariffTypePriceDynamic + } + return api.TariffTypePriceForecast }