From c347dfab33826189aaac43f76d09db3f7979dc54 Mon Sep 17 00:00:00 2001 From: motze92 <41009610+motze92@users.noreply.github.com> Date: Sun, 3 Mar 2024 10:50:06 +0100 Subject: [PATCH] Add PUN - Italienische Strompreise (#12262) --- evcc.dist.yaml | 4 + tariff/pun.go | 262 +++++++++++++++++++++++++++ templates/definition/tariff/pun.yaml | 8 + 3 files changed, 274 insertions(+) create mode 100644 tariff/pun.go create mode 100644 templates/definition/tariff/pun.yaml diff --git a/evcc.dist.yaml b/evcc.dist.yaml index cc860ec12..a0fa58fc2 100644 --- a/evcc.dist.yaml +++ b/evcc.dist.yaml @@ -183,6 +183,10 @@ tariffs: # charges: # optional, additional charges per kWh # tax: # optional, additional tax (0.1 for 10%) + # type: pun # PUN - Prezzo unico nazionale - Hourly Italian wholesale prices + # charges: 0 # optional, additional charges per kWh + # tax: 0 # optional, additional tax (0.1 for 10%) + # type: amber # token: # api token from https://app.amber.com.au/developers/ # siteid: # site ID returned by the API diff --git a/tariff/pun.go b/tariff/pun.go new file mode 100644 index 000000000..2b6eb75a2 --- /dev/null +++ b/tariff/pun.go @@ -0,0 +1,262 @@ +package tariff + +import ( + "encoding/xml" + "fmt" + "io" + "net/http" + "net/http/cookiejar" + "net/url" + "slices" + "strconv" + "strings" + "sync" + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "golang.org/x/net/html" +) + +type Pun struct { + *embed + log *util.Logger + charges float32 + tax float32 + data *util.Monitor[api.Rates] +} + +type NewDataSet struct { + XMLName xml.Name `xml:"NewDataSet"` + Prezzi []Prezzo `xml:"Prezzi"` +} + +type Prezzo struct { + Data string `xml:"Data"` + Ora string `xml:"Ora"` + PUN string `xml:"PUN"` +} + +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) +} + +func NewPunFromConfig(other map[string]interface{}) (api.Tariff, error) { + cc := struct { + Charges float32 + Tax float32 + }{ + Charges: 0.0, + Tax: 0.0, + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + t := &Pun{ + log: util.NewLogger("pun"), + charges: cc.Charges, + tax: cc.Tax, + data: util.NewMonitor[api.Rates](2 * time.Hour), + } + + done := make(chan error) + go t.run(done) + err := <-done + + return t, err +} + +func (t *Pun) run(done chan error) { + var once sync.Once + bo := newBackoff() + + for ; true; <-time.Tick(time.Hour) { + var today api.Rates + if err := backoff.Retry(func() error { + var err error + + today, err = t.getData(time.Now()) + + return err + }, bo); err != nil { + once.Do(func() { done <- err }) + + t.log.ERROR.Println(err) + continue + } + + var tomorrow api.Rates + if err := backoff.Retry(func() error { + var err error + + tomorrow, err = t.getData(time.Now().AddDate(0, 0, 1)) + + return err + }, bo); err != nil { + once.Do(func() { done <- err }) + t.log.ERROR.Println(err) + continue + } + + //merge today and tomorrow data + data := append(today, tomorrow...) + t.data.Set(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) getData(day time.Time) (api.Rates, error) { + // Initial URL + urlString := "https://www.mercatoelettrico.org/It/WebServerDataStore/MGP_Prezzi/" + day.Format("20060102") + "MGPPrezzi.xml" + + // Cookie Jar zur Speicherung von Cookies zwischen den Requests + jar, _ := cookiejar.New(nil) + client := &http.Client{ + Jar: jar, + } + + // Erster Request + resp, err := client.Get(urlString) + if err != nil { + fmt.Println("Error fetching URL:", err) + return nil, err + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + formData, err := parseFormFields(body) + if err != nil { + fmt.Println("Error parsing form fields:", err) + return nil, err + } + + redirectURL := resp.Request.URL.String() + + // Hinzufügen der spezifizierten Parameter + formData.Set("ctl00$ContentPlaceHolder1$CBAccetto1", "on") + formData.Set("ctl00$ContentPlaceHolder1$CBAccetto2", "on") + formData.Set("ctl00$ContentPlaceHolder1$Button1", "Accetto") + + // Formular senden + resp, err = client.PostForm(redirectURL, formData) + if err != nil { + fmt.Println("Error submitting form:", err) + return nil, err + } + defer resp.Body.Close() + + // Erneuter Request auf die ursprüngliche URL + resp, err = client.Get(urlString) + if err != nil { + fmt.Println("Error fetching URL after form submission:", err) + return nil, err + } + defer resp.Body.Close() + + // Verarbeitung der erhaltenen Daten + body, _ = io.ReadAll(resp.Body) + xmlData := []byte(string(body)) // Ersetzen Sie [Ihr XML-Datenstring hier] mit Ihrem XML-String + + var dataSet NewDataSet + err = xml.Unmarshal(xmlData, &dataSet) + if err != nil { + fmt.Println("Error unmarshalling XML: ", err) + } + + data := make(api.Rates, 0, len(dataSet.Prezzi)) + + for _, p := range dataSet.Prezzi { + date, err := time.Parse("20060102", p.Data) + if err != nil { + fmt.Println("Error parsing date: ", err) + } + + hour, err := strconv.Atoi(p.Ora) + if err != nil { + fmt.Println("Error parsing hour: ", err) + } + + location, err := time.LoadLocation("Europe/Rome") + if err != nil { + fmt.Println("Error loading location: ", err) + } + + start := time.Date(date.Year(), date.Month(), date.Day(), hour-1, 0, 0, 0, location) + end := start.Add(time.Hour) + + priceStr := strings.Replace(p.PUN, ",", ".", -1) // Ersetzen Sie Komma durch Punkt + price, err := strconv.ParseFloat(priceStr, 64) + if err != nil { + fmt.Println("Error parsing price: ", err) + } + + ar := api.Rate{ + Start: start, + End: end, + Price: (price/1000.0 + float64(t.charges)) * (1 + float64(t.tax)), + } + data = append(data, ar) + } + + data.Sort() + return data, nil +} + +func parseFormFields(body []byte) (url.Values, error) { + formData := url.Values{} + doc, err := html.Parse(strings.NewReader(string(body))) + if err != nil { + return formData, err + } + var f func(*html.Node) + f = func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == "input" { + inputType := "" + inputName := "" + inputValue := "" + for _, a := range n.Attr { + if a.Key == "type" { + inputType = a.Val + } else if a.Key == "name" { + inputName = a.Val + } else if a.Key == "value" { + inputValue = a.Val + } + } + if inputType == "hidden" && inputName != "" { + formData.Set(inputName, inputValue) + } + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + f(c) + } + } + f(doc) + return formData, nil +} diff --git a/templates/definition/tariff/pun.yaml b/templates/definition/tariff/pun.yaml new file mode 100644 index 000000000..a780b282f --- /dev/null +++ b/templates/definition/tariff/pun.yaml @@ -0,0 +1,8 @@ +template: pun +products: + - brand: PUN Orario +params: + - preset: tariff-base +render: | + type: pun + {{ include "tariff-base" . }}