Ctrl K
ComunidadGuías…

tariff/entsoe.go

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

Archivo original ↗Preguntar a KiBot
package tariff

import (
	"bytes"
	"encoding/xml"
	"errors"
	"slices"
	"strings"
	"sync"
	"time"

	"github.com/cenkalti/backoff/v4"
	"github.com/evcc-io/evcc/api"
	"github.com/evcc-io/evcc/tariff/entsoe"
	"github.com/evcc-io/evcc/util"
	"github.com/evcc-io/evcc/util/request"
	"github.com/evcc-io/evcc/util/transport"
)

type Entsoe struct {
	*request.Helper
	*embed
	log    *util.Logger
	token  string
	domain string
	data   *util.Monitor[api.Rates]
}

var _ api.Tariff = (*Entsoe)(nil)

func init() {
	registry.Add("entsoe", NewEntsoeFromConfig)
}

func NewEntsoeFromConfig(other map[string]any) (api.Tariff, error) {
	var cc struct {
		embed         `mapstructure:",squash"`
		Securitytoken string
		Domain        string
	}

	if err := util.DecodeOther(other, &cc); err != nil {
		return nil, err
	}

	if cc.Securitytoken == "" {
		return nil, errors.New("missing securitytoken")
	}

	if cc.Domain == "" {
		return nil, errors.New("missing domain")
	}

	if err := cc.init(); err != nil {
		return nil, err
	}

	domain, err := entsoe.Area(entsoe.BZN, strings.ToUpper(cc.Domain))
	if err != nil {
		return nil, err
	}

	log := util.NewLogger("entsoe").Redact(cc.Securitytoken)

	t := &Entsoe{
		log:    log,
		Helper: request.NewHelper(log),
		embed:  &cc.embed,
		token:  cc.Securitytoken,
		domain: domain,
		data:   util.NewMonitor[api.Rates](2 * time.Hour),
	}

	// Wrap the client with a decorator that adds the security token to each request.
	t.Client.Transport = &transport.Decorator{
		Base: t.Client.Transport,
		Decorator: transport.DecorateQuery(map[string]string{
			"securityToken": cc.Securitytoken,
		}),
	}

	return runOrError(t)
}

func (t *Entsoe) run(done chan error) {
	var once sync.Once

	// Data updated by ESO every half hour, but we only need data every hour to stay current.
	for tick := time.Tick(time.Hour); ; <-tick {
		var tr entsoe.PublicationMarketDocument

		if err := backoff.Retry(func() error {
			// Request the next 24 hours of data.
			data, err := t.DoBody(entsoe.DayAheadPricesRequest(t.domain, time.Hour*24))
			if err != nil {
				return backoffPermanentError(err)
			}

			var doc entsoe.Document
			if err := xml.NewDecoder(bytes.NewReader(data)).Decode(&doc); err != nil {
				return backoff.Permanent(err)
			}

			switch doc.XMLName.Local {
			case entsoe.AcknowledgementMarketDocumentName:
				var doc entsoe.AcknowledgementMarketDocument
				if err := xml.NewDecoder(bytes.NewReader(data)).Decode(&doc); err != nil {
					return backoff.Permanent(err)
				}

				return backoff.Permanent(errors.New(doc.Reason.Text))

			case entsoe.PublicationMarketDocumentName:
				if err := xml.NewDecoder(bytes.NewReader(data)).Decode(&tr); err != nil {
					return backoff.Permanent(err)
				}

				if tr.Type != string(entsoe.ProcessTypeDayAhead) {
					return backoff.Permanent(errors.New("invalid document type: " + tr.Type))
				}

				return nil

			default:
				return backoff.Permanent(errors.New("invalid document name: " + doc.XMLName.Local))
			}
		}, bo()); err != nil {
			if reportError(&once, done, err) {
				return
			}

			t.log.ERROR.Println(err)
			continue
		}

		if len(tr.TimeSeries) == 0 {
			if reportError(&once, done, entsoe.ErrInvalidData) {
				return
			}
			t.log.ERROR.Println(entsoe.ErrInvalidData)
			continue
		}

		// extract desired series
		res, err := entsoe.GetTsPriceData(tr.TimeSeries, entsoe.ResolutionQuarterHour)
		if err != nil {
			if reportError(&once, done, err) {
				return
			}
			t.log.ERROR.Println(err)
			continue
		}

		data := make(api.Rates, 0, len(res))
		for _, r := range res {
			ar := api.Rate{
				Start: r.Start.Local(),
				End:   r.End.Local(),
				Value: t.totalPrice(r.Value, r.Start),
			}
			data = append(data, ar)
		}

		mergeRates(t.data, data)
		once.Do(func() { close(done) })
	}
}

// Rates implements the api.Tariff interface
func (t *Entsoe) 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 *Entsoe) Type() api.TariffType {
	return api.TariffTypePriceForecast
}