Ctrl K
ComunidadGuías…

tariff/frankenergie.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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
}