Ctrl K
ComunidadGuías…

tariff/pun.go

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

Archivo original ↗Preguntar a KiBot
package tariff

import (
	"archive/zip"
	"bytes"
	"encoding/xml"
	"errors"
	"fmt"
	"net/http"
	"net/http/cookiejar"
	"slices"
	"strconv"
	"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"
)

// ErrPunDataNotAvailable indicates that GME has not yet published prices for the requested day.
var ErrPunDataNotAvailable = errors.New("PUN data not available")

// romeLocation is resolved once at package init to avoid repeated filesystem lookups.
var romeLocation *time.Location

type Pun struct {
	*embed
	zone string
	log  *util.Logger
	data *util.Monitor[api.Rates]
}

type NewDataSet struct {
	XMLName xml.Name `xml:"NewDataSet"`
	Prezzi  []Prezzo `xml:"Prezzi"`
}

type Zone struct {
	XMLName xml.Name
	Value   string `xml:",chardata"`
}

type Prezzo struct {
	Data  string `xml:"Data"`
	Ora   string `xml:"Ora"`
	Zones []Zone `xml:",any"`
}

type Rate struct {
	Start time.Time `json:"start"`
	End   time.Time `json:"end"`
	Price float64   `json:"price"`
}

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

func init() {
	registry.Add("pun", NewPunFromConfig)
	romeLocation, _ = time.LoadLocation("Europe/Rome")
}

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

	logger := util.NewLogger("pun")

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

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

	var zone = strings.ToUpper(strings.TrimSpace(cc.Zone))
	if cc.Zone == "" {
		zone = "PUN"
	}

	t := &Pun{
		log:   logger,
		zone:  zone,
		embed: &cc.embed,
		data:  util.NewMonitor[api.Rates](2 * time.Hour),
	}

	return runOrError(t)
}

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

	for tick := time.Tick(time.Hour); ; <-tick {
		// get today data
		today, err := backoff.RetryWithData(func() (api.Rates, error) {
			res, err := t.getData(time.Now())
			return res, backoffPermanentError(err)
		}, bo())
		if err != nil {
			if reportError(&once, done, err) {
				return
			}
			t.log.ERROR.Println(err)
			continue
		}

		// get tomorrow data (may not be available before ~13:00 CET)
		res, err := backoff.RetryWithData(func() (api.Rates, error) {
			res, err := t.getData(time.Now().AddDate(0, 0, 1))
			if errors.Is(err, ErrPunDataNotAvailable) {
				return res, backoff.Permanent(err)
			}
			return res, backoffPermanentError(err)
		}, bo())
		if err != nil && !errors.Is(err, ErrPunDataNotAvailable) {
			if reportError(&once, done, err) {
				return
			}
			t.log.ERROR.Println(err)
			continue
		}

		// merge today and tomorrow data
		data := append(today, res...)

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

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

func (t *Pun) priceForZone(p Prezzo) (float64, error) {
	for _, z := range p.Zones {
		if strings.ToUpper(strings.TrimSpace(z.XMLName.Local)) == t.zone {
			price, err := strconv.ParseFloat(strings.ReplaceAll(z.Value, ",", "."), 64)
			return price, err
		}
	}
	return 0, fmt.Errorf("zone %s not found for hour %s", t.zone, p.Ora)
}

func (t *Pun) getData(day time.Time) (api.Rates, error) {
	client := request.NewClient(t.log)
	client.Jar, _ = cookiejar.New(nil)

	// Request the ZIP file
	uri := "https://gme.mercatoelettrico.org/DesktopModules/GmeDownload/API/ExcelDownload/downloadzipfile?DataInizio=" + day.Format("20060102") + "&DataFine=" + day.Format("20060102") + "&Date=" + day.Format("20060102") + "&Mercato=MGP&Settore=Prezzi&FiltroDate=InizioFine"
	req, _ := http.NewRequest("GET", uri, nil)
	req.Header = http.Header{
		"Referer":            {"https://gme.mercatoelettrico.org/en-us/Home/Results/Electricity/MGP/Download?valore=Prezzi"},
		"moduleid":           {"12103"},
		"sec-ch-ua-mobile":   {"?0"},
		"sec-ch-ua-platform": {"Windows"},
		"sec-fetch-dest":     {"empty"},
		"sec-fetch-mode":     {"cors"},
		"sec-fetch-site":     {"same-origin"},
		"sec-gpc":            {"1"},
		"tabid":              {"1749"},
		"userid":             {"-1"},
	}

	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusNotFound {
		return nil, fmt.Errorf("%w: %s", ErrPunDataNotAvailable, day.Format("2006-01-02"))
	}

	body, err := request.ReadBody(resp)
	if err != nil {
		return nil, err
	}

	zipReader, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
	if err != nil {
		return nil, err
	}

	var tariffFile *zip.File
	for _, file := range zipReader.File {
		if strings.HasSuffix(file.Name, "Prezzi.xml") {
			tariffFile = file
			break
		}
	}
	if tariffFile == nil {
		return nil, fmt.Errorf("tariff file not found in downloaded ZIP archive")
	}

	f, err := tariffFile.Open()
	if err != nil {
		return nil, err
	}
	defer f.Close()

	// Process the received data
	var dataSet NewDataSet
	if err := xml.NewDecoder(f).Decode(&dataSet); err != nil {
		return nil, err
	}

	data := make(api.Rates, 0, len(dataSet.Prezzi))

	for _, p := range dataSet.Prezzi {
		date, err := time.Parse("20060102", p.Data)
		if err != nil {
			return nil, fmt.Errorf("parse date: %w", err)
		}

		hour, err := strconv.Atoi(p.Ora)
		if err != nil {
			return nil, fmt.Errorf("parse hour: %w", err)
		}

		// Adjust hour to handle edge case where p.Ora is "00"
		if hour == 0 {
			hour = 24
			date = date.AddDate(0, 0, -1)
		}

		price, err := t.priceForZone(p)
		if err != nil {
			return nil, fmt.Errorf("parse price: %w", err)
		}

		ts := time.Date(date.Year(), date.Month(), date.Day(), hour-1, 0, 0, 0, romeLocation)
		ar := api.Rate{
			Start: ts,
			End:   ts.Add(time.Hour),
			Value: t.totalPrice(price/1e3, ts),
		}
		data = append(data, ar)
	}

	data.Sort()
	return data, nil
}