From 532f77fdeaf2d985234d598835647ef47b5f4061 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 10 Oct 2024 12:34:02 +0200 Subject: [PATCH] Entsoe: handle multiple timeseries and gaps (#16592) --- tariff/entsoe/api.go | 83 +++++++++++++++++++++++++---------------- tariff/entsoe/static.go | 34 +++++++++-------- 2 files changed, 69 insertions(+), 48 deletions(-) diff --git a/tariff/entsoe/api.go b/tariff/entsoe/api.go index c439e437c..694d8425a 100644 --- a/tariff/entsoe/api.go +++ b/tariff/entsoe/api.go @@ -11,6 +11,7 @@ import ( "github.com/dylanmei/iso8601" "github.com/evcc-io/evcc/util/request" + "github.com/samber/lo" ) const ( @@ -53,17 +54,23 @@ type Rate struct { func GetTsPriceData(ts []TimeSeries, resolution ResolutionType) ([]Rate, error) { var res []Rate - for _, v := range ts { - if v.Period.Resolution != resolution { - continue + for _, ts := range ts { + if unit := ts.PriceMeasureUnitName; unit != "MWH" { + return nil, fmt.Errorf("%w: invalid unit: %s", ErrInvalidData, unit) } - data, err := ExtractTsPriceData(&v) - if err != nil { - return nil, err - } + for _, period := range ts.Period { + if period.Resolution != resolution { + continue + } - res = append(res, data...) + data, err := ExtractPeriodPriceData(&period) + if err != nil { + return nil, err + } + + res = append(res, data...) + } } if len(res) == 0 { @@ -73,40 +80,52 @@ func GetTsPriceData(ts []TimeSeries, resolution ResolutionType) ([]Rate, error) return res, nil } -// ExtractTsPriceData massages the given TimeSeries data set to provide Rate entries with associated start and end timestamps. -func ExtractTsPriceData(timeseries *TimeSeries) ([]Rate, error) { - data := make([]Rate, 0, len(timeseries.Period.Point)) +// ExtractPeriodPriceData massages the given Period data set to provide Rate entries with associated start and end timestamps. +func ExtractPeriodPriceData(period *TimeSeriesPeriod) ([]Rate, error) { + data := make([]Rate, 0, len(period.Point)) - duration, err := iso8601.ParseDuration(string(timeseries.Period.Resolution)) + duration, err := iso8601.ParseDuration(string(period.Resolution)) if err != nil { return nil, err } - if unit := timeseries.PriceMeasureUnitName; unit != "MWH" { - return nil, fmt.Errorf("%w: invalid unit: %s", ErrInvalidData, unit) + var count int + switch period.Resolution { + case ResolutionHour: + count = 24 + case ResolutionHalfHour: + count = 2 * 24 + case ResolutionQuarterHour: + count = 4 * 24 + default: + return nil, fmt.Errorf("%w: invalid resolution: %v", ErrInvalidData, period.Resolution) } - ts := timeseries.Period.TimeInterval.Start.Time - for _, point := range timeseries.Period.Point { - d := Rate{ - Value: point.PriceAmount / 1e3, // Price/MWh to Price/kWh - Start: ts, + ts := period.TimeInterval.Start.Time + points := lo.SliceToMap(period.Point, func(p Point) (int, Point) { + return p.Position, p + }) + + for pos := 1; pos <= count; pos++ { + var point Point + for last := pos; last > 0; last-- { + if p, ok := points[last]; ok { + point = p + break + } } - // Nudge pointer on as required by defined data resolution - switch timeseries.Period.Resolution { - case ResolutionQuarterHour, ResolutionHalfHour, ResolutionHour: - ts = ts.Add(duration) - case ResolutionDay: - ts = ts.AddDate(0, 0, 1) - case ResolutionWeek: - ts = ts.AddDate(0, 0, 7) - case ResolutionYear: - ts = ts.AddDate(1, 0, 0) - default: - return nil, fmt.Errorf("%w: invalid resolution: %v", ErrInvalidData, timeseries.Period.Resolution) + if point.Position == 0 { + return nil, fmt.Errorf("%w: missing point at position: %d", ErrInvalidData, pos) + } + + start := ts.Add(time.Duration(pos-1) * duration) + + d := Rate{ + Value: point.PriceAmount / 1e3, // Price/MWh to Price/kWh + Start: start, + End: start.Add(duration), } - d.End = ts data = append(data, d) } diff --git a/tariff/entsoe/static.go b/tariff/entsoe/static.go index 43be5cb36..3a6bc4c1e 100644 --- a/tariff/entsoe/static.go +++ b/tariff/entsoe/static.go @@ -101,16 +101,16 @@ type TimeSeries struct { Text string `xml:",chardata"` // 10YCZ-CEPS-----N, 10YCZ-C... CodingScheme string `xml:"codingScheme,attr"` } `xml:"out_Domain.mRID"` - CurrencyUnitName string `xml:"currency_Unit.name"` // EUR, EUR, EUR, EUR, EUR, ... - PriceMeasureUnitName string `xml:"price_Measure_Unit.name"` // MWH, MWH, MWH, MWH, MWH, ... - CurveType string `xml:"curveType"` // A01, A01, A01, A01, A01, ... - Period TimeSeriesPeriod `xml:"Period"` - AuctionType string `xml:"auction.type"` // A01, A01, A01, A01, A01, ... - ContractMarketAgreementType string `xml:"contract_MarketAgreement.type"` // A01, A01, A01, A01, A01, ... - QuantityMeasureUnitName string `xml:"quantity_Measure_Unit.name"` // MAW, MAW, MAW, MAW, MAW, ... - AuctionMRID string `xml:"auction.mRID"` // CP_A_Hourly_SK-UA, CP_A_D... - AuctionCategory string `xml:"auction.category"` // A04, A04, A01, A01, A01, ... - ClassificationSequenceAttributeInstanceComponentPosition string `xml:"classificationSequence_AttributeInstanceComponent.position"` // 1, 1 + CurrencyUnitName string `xml:"currency_Unit.name"` // EUR, EUR, EUR, EUR, EUR, ... + PriceMeasureUnitName string `xml:"price_Measure_Unit.name"` // MWH, MWH, MWH, MWH, MWH, ... + CurveType string `xml:"curveType"` // A01, A01, A01, A01, A01, ... + Period []TimeSeriesPeriod `xml:"Period"` + AuctionType string `xml:"auction.type"` // A01, A01, A01, A01, A01, ... + ContractMarketAgreementType string `xml:"contract_MarketAgreement.type"` // A01, A01, A01, A01, A01, ... + QuantityMeasureUnitName string `xml:"quantity_Measure_Unit.name"` // MAW, MAW, MAW, MAW, MAW, ... + AuctionMRID string `xml:"auction.mRID"` // CP_A_Hourly_SK-UA, CP_A_D... + AuctionCategory string `xml:"auction.category"` // A04, A04, A01, A01, A01, ... + ClassificationSequenceAttributeInstanceComponentPosition string `xml:"classificationSequence_AttributeInstanceComponent.position"` // 1, 1 } type TimeSeriesPeriod struct { @@ -121,10 +121,12 @@ type TimeSeriesPeriod struct { End shortrfc3339.Timestamp `xml:"end"` // 2016-01-01T23:00Z, 2016-0... } `xml:"timeInterval"` Resolution ResolutionType `xml:"resolution"` // PT60M, PT60M, PT60M, PT60... - Point []struct { - Text string `xml:",chardata"` - Position int `xml:"position"` // 1, 2, 3, 4, 5, 6, 7, 8, 9... - PriceAmount float64 `xml:"price.amount"` // 16.50, 15.50, 14.00, 10.0... - Quantity string `xml:"quantity"` // 226, 87, 104, 189, 217, 8... - } `xml:"Point"` + Point []Point `xml:"Point"` +} + +type Point struct { + Text string `xml:",chardata"` + Position int `xml:"position"` // 1, 2, 3, 4, 5, 6, 7, 8, 9... + PriceAmount float64 `xml:"price.amount"` // 16.50, 15.50, 14.00, 10.0... + Quantity string `xml:"quantity"` // 226, 87, 104, 189, 217, 8... }