Tariff: add forecast provider (#13451)
This commit is contained in:
parent
9d8e2b19d5
commit
f483f2c74b
1 changed files with 90 additions and 9 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue