diff --git a/power_prices/power_prices.go b/power_prices/power_prices.go index cc5750d..1056c5f 100644 --- a/power_prices/power_prices.go +++ b/power_prices/power_prices.go @@ -11,11 +11,11 @@ import ( ) type PowerPriceSourceConfigs struct { - AllInPowerConfig AllInPowerConfig `json:"all-in-power"` + Entsoe EntsoeConfig `mapstructure:"entsoe"` } type Config struct { - Sources PowerPriceSourceConfigs `json:"sources"` + Sources PowerPriceSourceConfigs `mapstructure:"sources"` } type PowerPrices struct { @@ -45,7 +45,8 @@ func New( logger *logrus.Logger, victoriaMetrics *victoria_metrics.VictoriaMetrics, ) *PowerPrices { - sources = append(sources, newAllInPower(config.Sources.AllInPowerConfig)) + sources = append(sources, newAllInPower()) + sources = append(sources, newEntsoe(config.Sources.Entsoe)) return &PowerPrices{ Config: config, @@ -57,7 +58,7 @@ func New( } func (pp *PowerPrices) addPricesOfSource(source PowerPriceSource) error { - prices, err := source.GetPricesKwH(time.Now().Add(24 * time.Hour)) + prices, err := source.GetPricesKwH(time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)) if err != nil { return err } @@ -98,6 +99,10 @@ func (pp *PowerPrices) updateMetrics() error { func (pp *PowerPrices) Start() { pp.logger.Info("Starting power price collector") + err := pp.updateMetrics() + if err != nil { + pp.errChannel <- err + } now := time.Now() nextMidnight := time.Date(now.Year(), now.Month(), now.Day()+1, 1, 0, 0, 0, time.Local) @@ -105,10 +110,5 @@ func (pp *PowerPrices) Start() { time.Sleep(durationUntilMidnight) - err := pp.updateMetrics() - if err != nil { - pp.errChannel <- err - } - go pp.Start() } diff --git a/power_prices/power_prices_test.go b/power_prices/power_prices_test.go new file mode 100644 index 0000000..1dd4bbd --- /dev/null +++ b/power_prices/power_prices_test.go @@ -0,0 +1,20 @@ +package power_prices + +// import ( +// "time" +// "testing" +// ) + +// func Test_Entsoe_GetPricesKwH(t *testing.T) { +// entsoeClient := newEntsoe(EntsoeConfig{ +// Domain: "", +// SecurityToken: "", +// }) + +// prices, err := entsoeClient.GetPricesKwH(time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)) +// if err != nil { +// t.Fatalf("Error getting prices: %s", err) +// } + +// t.Log(prices) +// } \ No newline at end of file diff --git a/power_prices/source_allinpower.go b/power_prices/source_allinpower.go index 081f433..4ed4a8b 100644 --- a/power_prices/source_allinpower.go +++ b/power_prices/source_allinpower.go @@ -1,26 +1,21 @@ package power_prices import ( + "fmt" "time" "net/http" "io/ioutil" "encoding/json" ) -type AllInPowerConfig struct {} - type AllInPower struct { name string - Config AllInPowerConfig - Resolution uint Client *http.Client } -func newAllInPower(config AllInPowerConfig) *AllInPower { +func newAllInPower() *AllInPower { return &AllInPower{ name: "all-in-power", - Config: config, - Resolution: 60, Client: &http.Client{}, } } @@ -29,7 +24,7 @@ func (a *AllInPower) GetName() string { return a.name } -type spotMarketPriceResponse struct { +type allInPowerSpotMarketPriceResponse struct { Id int `json:"id"` Timestamps []string `json:"timestamps"` Created string `json:"created"` @@ -41,7 +36,8 @@ type spotMarketPriceResponse struct { } func (a *AllInPower) GetPricesKwH(timestamp time.Time) (map[time.Time]float64, error) { - request, err := http.NewRequest("GET", "https://api.allinpower.nl/troodon/api/p/spot_market/prices/?date="+timestamp.Format(time.DateOnly)+"&product_type=ELK", nil) + requestUrl := fmt.Sprintf("https://api.allinpower.nl/troodon/api/p/spot_market/prices/?date=%s&product_type=ELK", timestamp.Add(24 * time.Hour).Format(time.DateOnly)) + request, err := http.NewRequest("GET", requestUrl, nil) if err != nil { return nil, err } @@ -62,7 +58,7 @@ func (a *AllInPower) GetPricesKwH(timestamp time.Time) (map[time.Time]float64, e return nil, err } - var responseBody spotMarketPriceResponse + var responseBody allInPowerSpotMarketPriceResponse err = json.Unmarshal(body, &responseBody) if err != nil { return nil, err diff --git a/power_prices/source_entsoe.go b/power_prices/source_entsoe.go new file mode 100644 index 0000000..8a1b9ab --- /dev/null +++ b/power_prices/source_entsoe.go @@ -0,0 +1,139 @@ +package power_prices + +import ( + "fmt" + "time" + "strconv" + "net/http" + "io/ioutil" + "encoding/xml" +) + +const entsoeTimestampFormat = "200601021504" + +type EntsoeConfig struct { + SecurityToken string `mapstructure:"security-token"` + Domain string `mapstructure:"domain"` +} + +type Entsoe struct { + name string + Client *http.Client + Config EntsoeConfig +} + +func newEntsoe(config EntsoeConfig) *Entsoe { + return &Entsoe{ + name: "entsoe", + Client: &http.Client{}, + Config: config, + } +} + +func (e *Entsoe) GetName() string { + return e.name +} + +type entsoeDayAheadResponse struct { + MRID string `xml:"mRID"` + RevisionNumber string `xml:"revisionNumber"` + Type string `xml:"type"` + SenderMarketParticipantMRID struct { + Text string `xml:",chardata"` + CodingScheme string `xml:"codingScheme,attr"` + } `xml:"sender_MarketParticipant.mRID"` + SenderMarketParticipantMarketRoleType string `xml:"sender_MarketParticipant.marketRole.type"` + ReceiverMarketParticipantMRID struct { + Text string `xml:",chardata"` + CodingScheme string `xml:"codingScheme,attr"` + } `xml:"receiver_MarketParticipant.mRID"` + ReceiverMarketParticipantMarketRoleType string `xml:"receiver_MarketParticipant.marketRole.type"` + CreatedDateTime string `xml:"createdDateTime"` + PeriodTimeInterval struct { + Text string `xml:",chardata"` + Start string `xml:"start"` + End string `xml:"end"` + } `xml:"period.timeInterval"` + TimeSeries []struct { + Text string `xml:",chardata"` + MRID string `xml:"mRID"` + BusinessType string `xml:"businessType"` + InDomainMRID struct { + Text string `xml:",chardata"` + CodingScheme string `xml:"codingScheme,attr"` + } `xml:"in_Domain.mRID"` + OutDomainMRID struct { + Text string `xml:",chardata"` + CodingScheme string `xml:"codingScheme,attr"` + } `xml:"out_Domain.mRID"` + CurrencyUnitName string `xml:"currency_Unit.name"` + PriceMeasureUnitName string `xml:"price_Measure_Unit.name"` + CurveType string `xml:"curveType"` + Period struct { + Text string `xml:",chardata"` + TimeInterval struct { + Text string `xml:",chardata"` + Start string `xml:"start"` + End string `xml:"end"` + } `xml:"timeInterval"` + Resolution string `xml:"resolution"` + Point []struct { + Text string `xml:",chardata"` + Position string `xml:"position"` + PriceAmount string `xml:"price.amount"` + } `xml:"Point"` + } `xml:"Period"` + } `xml:"TimeSeries"` +} + +func (e *Entsoe) GetPricesKwH(timestamp time.Time) (map[time.Time]float64, error) { + startTimestamp := timestamp.Format(entsoeTimestampFormat) + endTimestamp := timestamp.Add(24 * time.Hour).Format(entsoeTimestampFormat) + + requestUrl := fmt.Sprintf("https://web-api.tp.entsoe.eu/api?securityToken=%s&documentType=A44&in_Domain=%s&out_Domain=%s&periodStart=%s&periodEnd=%s", e.Config.SecurityToken, e.Config.Domain, e.Config.Domain, startTimestamp, endTimestamp) + request, err := http.NewRequest("GET", requestUrl, nil) + if err != nil { + return nil, err + } + request.Header.Del("User-Agent") + + response, err := e.Client.Do(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, ErrFailedToRetrieveData + } + + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, err + } + + var responseBody entsoeDayAheadResponse + err = xml.Unmarshal(body, &responseBody) + if err != nil { + return nil, err + } + + prices := make(map[time.Time]float64) + startingTimestamp, err := time.Parse("2006-01-02T15:04Z", responseBody.TimeSeries[1].Period.TimeInterval.Start) + if err != nil { + return nil, err + } + + for i, entry := range responseBody.TimeSeries[1].Period.Point { + timestamp := startingTimestamp.Add(time.Duration(i) * time.Hour) + + price, err := strconv.ParseFloat(entry.PriceAmount, 2) + if err != nil { + return nil, err + } + + prices[timestamp] = price / 1000 + } + + return prices, nil +}