From 9d4bc2aea74a21a5c6d3bac4dae6d7f03c64f68d Mon Sep 17 00:00:00 2001 From: Martin Valov Date: Mon, 3 Jul 2023 10:33:37 +0200 Subject: [PATCH] Add support for Energinet (#8717) --- evcc.dist.yaml | 5 ++ tariff/energinet.go | 109 ++++++++++++++++++++++++++++++++++++++ tariff/energinet/types.go | 15 ++++++ 3 files changed, 129 insertions(+) create mode 100644 tariff/energinet.go create mode 100644 tariff/energinet/types.go diff --git a/evcc.dist.yaml b/evcc.dist.yaml index 60f99834b..03151fc08 100644 --- a/evcc.dist.yaml +++ b/evcc.dist.yaml @@ -179,6 +179,11 @@ tariffs: # region: ee # or lt, lv, fi # charges: # optional, additional charges per kWh # tax: # optional, additional tax (0.1 for 10%) + + # type: energinet # Energinet using the price in DKK + # region: dk1 # or dk2 + # charges: # optional, additional charges per kWh + # tax: # optional, additional tax (0.1 for 10%) feedin: # rate for feeding excess (pv) energy to the grid type: fixed diff --git a/tariff/energinet.go b/tariff/energinet.go new file mode 100644 index 000000000..a41cff590 --- /dev/null +++ b/tariff/energinet.go @@ -0,0 +1,109 @@ +package tariff + +import ( + "errors" + "fmt" + "strings" + "sync" + "time" + + "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" + "golang.org/x/exp/slices" +) + +type Energinet struct { + *embed + mux sync.Mutex + log *util.Logger + region string + data api.Rates + updated time.Time +} + +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") + } + + t := &Energinet{ + embed: &cc.embed, + log: util.NewLogger("energinet"), + region: strings.ToLower(cc.Region), + } + + done := make(chan error) + go t.run(done) + err := <-done + + return t, err +} + +func (t *Energinet) run(done chan error) { + var once sync.Once + client := request.NewHelper(t.log) + + for ; true; <-time.Tick(time.Hour) { + var res energinet.Prices + + ts := time.Now().Truncate(time.Hour) + uri := fmt.Sprintf(energinet.URI, + ts.Format(time.RFC3339), + ts.Add(24*time.Hour).Format(time.RFC3339), + t.region) + + if err := client.GetJSON(uri, &res); err != nil { + once.Do(func() { done <- err }) + + t.log.ERROR.Println(err) + continue + } + + once.Do(func() { close(done) }) + + t.mux.Lock() + t.updated = time.Now() + + t.data = make(api.Rates, 0, len(res.Records)) + for _, r := range res.Records { + date, _ := time.Parse("2006-01-02T15:04:05", r.HourUTC) + ar := api.Rate{ + Start: date.Local(), + End: date.Add(time.Hour).Local(), + Price: t.totalPrice(r.SpotPriceDKK / 1e3), + } + t.data = append(t.data, ar) + } + + t.mux.Unlock() + } +} + +// Rates implements the api.Tariff interface +func (t *Energinet) Rates() (api.Rates, error) { + t.mux.Lock() + defer t.mux.Unlock() + return slices.Clone(t.data), outdatedError(t.updated, time.Hour) +} + +// Type returns the tariff type +func (t *Energinet) Type() api.TariffType { + return api.TariffTypePriceDynamic +} diff --git a/tariff/energinet/types.go b/tariff/energinet/types.go new file mode 100644 index 000000000..06062f330 --- /dev/null +++ b/tariff/energinet/types.go @@ -0,0 +1,15 @@ +package energinet + +const URI = "https://api.energidataservice.dk/dataset/Elspotprices?offset=0&start=%s&end=%s&filter={\"PriceArea\":[\"%s\"]}&timezone=dk&limit=48" + +type Prices struct { + Records []PriceInfo `json:"records"` +} + +type PriceInfo struct { + HourUTC string + HourDK string + PriceArea string + SpotPriceDKK float64 + SpotPriceEUR float64 +}