diff --git a/tariff/energinet.go b/tariff/energinet.go new file mode 100644 index 000000000..410757486 --- /dev/null +++ b/tariff/energinet.go @@ -0,0 +1,111 @@ +package tariff + +import ( + "errors" + "fmt" + "slices" + "strings" + "sync" + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/tariff/energinet" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" +) + +type Energinet struct { + *embed + log *util.Logger + region string + data *util.Monitor[api.Rates] +} + +var _ api.Tariff = (*Energinet)(nil) + +func init() { + registry.Add("energinet", NewEnerginetFromConfig) +} + +func NewEnerginetFromConfig(other map[string]interface{}) (api.Tariff, error) { + var cc struct { + embed `mapstructure:",squash"` + Region string + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + if cc.Region == "" { + return nil, errors.New("missing region") + } + + if err := cc.init(); err != nil { + return nil, err + } + + t := &Energinet{ + embed: &cc.embed, + log: util.NewLogger("energinet"), + region: strings.ToLower(cc.Region), + data: util.NewMonitor[api.Rates](2 * time.Hour), + } + + t.log.WARN.Println("tariff is deprecated and will be removed in a future version") + + return runOrError(t) +} + +func (t *Energinet) run(done chan error) { + var once sync.Once + client := request.NewHelper(t.log) + + for tick := time.Tick(time.Hour); ; <-tick { + var res energinet.Prices + + ts := time.Now().Truncate(time.Hour) + uri := fmt.Sprintf(energinet.URI, + ts.Format(energinet.TimeFormat), + ts.Add(24*time.Hour).Format(energinet.TimeFormat), + t.region) + + if err := backoff.Retry(func() error { + return backoffPermanentError(client.GetJSON(uri, &res)) + }, bo()); err != nil { + once.Do(func() { done <- err }) + + t.log.ERROR.Println(err) + continue + } + + data := make(api.Rates, 0, len(res.Records)) + for _, r := range res.Records { + ts, _ := time.Parse("2006-01-02T15:04:05", r.HourUTC) + ar := api.Rate{ + Start: ts.Local(), + End: ts.Add(time.Hour).Local(), + Value: t.totalPrice(r.SpotPriceDKK/1e3, ts), + } + data = append(data, ar) + } + + mergeRates(t.data, data) + once.Do(func() { close(done) }) + } +} + +// Rates implements the api.Tariff interface +func (t *Energinet) Rates() (api.Rates, error) { + var res api.Rates + err := t.data.GetFunc(func(val api.Rates) { + res = slices.Clone(val) + }) + return res, err +} + +// Type implements the api.Tariff interface +func (t *Energinet) Type() api.TariffType { + return api.TariffTypePriceForecast +} diff --git a/templates/definition/tariff/energinet-price.yaml b/templates/definition/tariff/energinet-price.yaml index b43b5f742..d5fab0580 100644 --- a/templates/definition/tariff/energinet-price.yaml +++ b/templates/definition/tariff/energinet-price.yaml @@ -1,5 +1,4 @@ template: energinet-price -covers: ["energinet"] products: - brand: Energinet requirements: diff --git a/templates/definition/tariff/energinet.yaml b/templates/definition/tariff/energinet.yaml new file mode 100644 index 000000000..4a033609c --- /dev/null +++ b/templates/definition/tariff/energinet.yaml @@ -0,0 +1,19 @@ +template: energinet +deprecated: true +products: + - brand: Energinet +requirements: + evcc: ["skiptest"] +group: price +countries: ["DK"] +params: + - name: region + example: dk1 + type: choice + choice: ["dk1", "dk2"] + required: true + - preset: tariff-base +render: | + type: energinet + region: {{ .region }} + {{ include "tariff-base" . }}