tariff/frankenergie.go
Rama master · réplica de lectura del repositorio original.
package tariff
import (
"errors"
"fmt"
"net/http"
"slices"
"strings"
"sync"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
)
const frankEnergieURI = "https://graphql.frankenergie.nl/"
const frankEnergieQuery = `query MarketPrices($date: String!, $resolution: PriceResolution!) {
marketPrices(date: $date, resolution: $resolution) {
electricityPrices { from till marketPrice allInPrice }
}
}`
type FrankEnergie struct {
*embed
log *util.Logger
export bool
loc *time.Location
data *util.Monitor[api.Rates]
}
type frankEnergiePrice struct {
From time.Time
Till time.Time
MarketPrice float64
AllInPrice float64
}
type frankEnergieResponse struct {
Data struct {
MarketPrices struct {
ElectricityPrices []frankEnergiePrice
}
}
Errors []struct {
Message string
}
}
var _ api.Tariff = (*FrankEnergie)(nil)
func init() {
registry.Add("frank-energie", NewFrankEnergieFromConfig)
}
func NewFrankEnergieFromConfig(other map[string]any) (api.Tariff, error) {
var cc struct {
embed `mapstructure:",squash"`
TariffDirection string
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
var export bool
switch strings.ToLower(cc.TariffDirection) {
case "", "import":
case "export":
export = true
default:
return nil, fmt.Errorf("invalid tariff direction: %s", cc.TariffDirection)
}
if err := cc.init(); err != nil {
return nil, err
}
loc, err := time.LoadLocation("Europe/Amsterdam")
if err != nil {
return nil, err
}
t := &FrankEnergie{
embed: &cc.embed,
log: util.NewLogger("frank-energie"),
export: export,
loc: loc,
data: util.NewMonitor[api.Rates](2 * time.Hour),
}
return runOrError(t)
}
func (t *FrankEnergie) run(done chan error) {
var once sync.Once
client := request.NewHelper(t.log)
for tick := time.Tick(time.Hour); ; <-tick {
today := time.Now().In(t.loc)
var prices []frankEnergiePrice
if err := backoff.Retry(func() error {
var err error
prices, err = t.prices(client, today)
return backoffPermanentError(err)
}, bo()); err != nil {
if reportError(&once, done, err) {
return
}
t.log.ERROR.Println(err)
continue
}
res := t.rates(prices)
// day-ahead prices for tomorrow are published around 13:00
next, err := t.prices(client, today.AddDate(0, 0, 1))
if err == nil {
res = append(res, t.rates(next)...)
} else {
t.log.TRACE.Println("tomorrow:", err)
}
t.data.SetFunc(func(old api.Rates) api.Rates {
// keep previously fetched slots for tomorrow if the update failed
if err != nil {
res = append(res, ratesAfter(old, res[len(res)-1].End)...)
}
return mergeAfter(old, res, time.Now().Truncate(SlotDuration))
})
once.Do(func() { close(done) })
}
}
func (t *FrankEnergie) prices(client *request.Helper, day time.Time) ([]frankEnergiePrice, error) {
data := map[string]any{
"query": frankEnergieQuery,
"variables": map[string]string{
"date": day.Format(time.DateOnly),
"resolution": "PT15M",
},
}
req, err := request.New(http.MethodPost, frankEnergieURI, request.MarshalJSON(data), request.JSONEncoding)
if err != nil {
return nil, err
}
var res frankEnergieResponse
if err := client.DoJSON(req, &res); err != nil {
return nil, err
}
if len(res.Errors) > 0 {
return nil, errors.New(res.Errors[0].Message)
}
if len(res.Data.MarketPrices.ElectricityPrices) == 0 {
return nil, errors.New("no prices")
}
return res.Data.MarketPrices.ElectricityPrices, nil
}
// ratesAfter returns the rates starting at or after ts
func ratesAfter(rates api.Rates, ts time.Time) api.Rates {
var res api.Rates
for _, r := range rates {
if !r.Start.Before(ts) {
res = append(res, r)
}
}
return res
}
func (t *FrankEnergie) rates(prices []frankEnergiePrice) api.Rates {
res := make(api.Rates, 0, len(prices))
for _, p := range prices {
price := p.AllInPrice
if t.export || t.Charges != 0 || len(t.chargesZones) > 0 || t.Tax != 0 || t.Formula != "" {
price = t.totalPrice(p.MarketPrice, p.From)
}
res = append(res, api.Rate{
Start: p.From.Local(),
End: p.Till.Local(),
Value: price,
})
}
return res
}
// Rates implements the api.Tariff interface
func (t *FrankEnergie) 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 *FrankEnergie) Type() api.TariffType {
return api.TariffTypePriceForecast
}