Skip to content

Commit

Permalink
Merge pull request #206 from Pluimvee/Full-support-for-PT15M
Browse files Browse the repository at this point in the history
Full support for PT15M resolution and handle overlapping timeseries
  • Loading branch information
JaccoR authored Nov 19, 2024
2 parents 557010f + d190439 commit f666ae5
Show file tree
Hide file tree
Showing 8 changed files with 1,943 additions and 46 deletions.
152 changes: 106 additions & 46 deletions custom_components/entsoe/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,52 +70,7 @@ def query_day_ahead_prices(

if response.status_code == 200:
try:
root = self._remove_namespace(ET.fromstring(response.content))
_LOGGER.debug(f"content: {root}")
series = {}

# Extract TimeSeries data
for timeseries in root.findall(".//TimeSeries"):
for period in timeseries.findall(".//Period"):
resolution = period.find(".//resolution").text

if resolution != "PT60M":
continue

response_start = period.find(".//timeInterval/start").text
start_time = (
datetime.strptime(response_start, "%Y-%m-%dT%H:%MZ")
.replace(tzinfo=pytz.UTC)
.astimezone()
)

response_end = period.find(".//timeInterval/end").text
end_time = (
datetime.strptime(response_end, "%Y-%m-%dT%H:%MZ")
.replace(tzinfo=pytz.UTC)
.astimezone()
)

_LOGGER.debug(f"Period found is from {start_time} till {end_time}")

for point in period.findall(".//Point"):
position = point.find(".//position").text
price = point.find(".//price.amount").text
hour = int(position) - 1
series[start_time + timedelta(hours=hour)] = float(price)

# Now fill in any missing hours
current_time = start_time
last_price = series[current_time]

while current_time < end_time: # upto excluding! the endtime
if current_time in series:
last_price = series[current_time] # Update to the current price
else:
_LOGGER.debug(f"Extending the price {last_price} of the previous hour to {current_time}")
series[current_time] = last_price # Fill with the last known price
current_time += timedelta(hours=1)

series = self.parse_price_document(response.content)
return dict(sorted(series.items()))

except Exception as exc:
Expand All @@ -125,6 +80,111 @@ def query_day_ahead_prices(
print(f"Failed to retrieve data: {response.status_code}")
return None

# lets process the received document
def parse_price_document(self, document: str) -> str:

root = self._remove_namespace(ET.fromstring(document))
_LOGGER.debug(f"content: {root}")
series = {}

# for all given timeseries in this response
# There may be overlapping times in the repsonse. For now we skip timeseries which we already processed
for timeseries in root.findall(".//TimeSeries"):

# for all periods in this timeseries.....-> we still asume the time intervals do not overlap, and are in sequence
for period in timeseries.findall(".//Period"):
# there can be different resolutions for each period (BE casus in which historical is quarterly and future is hourly)
resolution = period.find(".//resolution").text

# for now supporting 60 and 15 minutes resolutions (ISO8601 defined)
if resolution == "PT60M" or resolution == "PT1H":
resolution = "PT60M"
elif resolution != "PT15M":
continue

response_start = period.find(".//timeInterval/start").text
start_time = (
datetime.strptime(response_start, "%Y-%m-%dT%H:%MZ")
.replace(tzinfo=pytz.UTC)
.astimezone()
)
start_time.replace(minute=0) # ensure we start from the whole hour

response_end = period.find(".//timeInterval/end").text
end_time = (
datetime.strptime(response_end, "%Y-%m-%dT%H:%MZ")
.replace(tzinfo=pytz.UTC)
.astimezone()
)
_LOGGER.debug(
f"Period found is from {start_time} till {end_time} with resolution {resolution}"
)
if start_time in series:
_LOGGER.debug(
"We found a duplicate period in the response, possibly with another resolution. We skip this period"
)
continue

if resolution == "PT60M":
series.update(self.process_PT60M_points(period, start_time))
else:
series.update(self.process_PT15M_points(period, start_time))

# Now fill in any missing hours
current_time = start_time
last_price = series[current_time]

while current_time < end_time: # upto excluding! the endtime
if current_time in series:
last_price = series[current_time] # Update to the current price
else:
_LOGGER.debug(
f"Extending the price {last_price} of the previous hour to {current_time}"
)
series[current_time] = (
last_price # Fill with the last known price
)
current_time += timedelta(hours=1)

return series

# processing hourly prices info -> thats easy
def process_PT60M_points(self, period: Element, start_time: datetime):
data = {}
for point in period.findall(".//Point"):
position = point.find(".//position").text
price = point.find(".//price.amount").text
hour = int(position) - 1
time = start_time + timedelta(hours=hour)
data[time] = float(price)
return data

# processing quarterly prices -> this is more complex
def process_PT15M_points(self, period: Element, start_time: datetime):
positions = {}

# first store all positions
for point in period.findall(".//Point"):
position = point.find(".//position").text
price = point.find(".//price.amount").text
positions[int(position)] = float(price)

# now calculate hourly averages based on available points
data = {}
last_hour = (max(positions.keys()) + 3) // 4
last_price = 0

for hour in range(last_hour):
sum_prices = 0
for idx in range(hour * 4 + 1, hour * 4 + 5):
last_price = positions.get(idx, last_price)
sum_prices += last_price

time = start_time + timedelta(hours=hour)
data[time] = round(sum_prices / 4, 2)

return data


class Area(enum.Enum):
"""
Expand Down
Empty file.
72 changes: 72 additions & 0 deletions custom_components/entsoe/test/datasets/BE_15M_avg.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<Publication_MarketDocument xmlns="urn:iec62325.351:tc57wg16:451-3:publicationdocument:7:3">
<mRID>64e2af3a87c2404cbea80edc067a1b6f</mRID>
<revisionNumber>1</revisionNumber>
<type>A44</type>
<sender_MarketParticipant.mRID codingScheme="A01">10X1001A1001A450</sender_MarketParticipant.mRID>
<sender_MarketParticipant.marketRole.type>A32</sender_MarketParticipant.marketRole.type>
<receiver_MarketParticipant.mRID codingScheme="A01">10X1001A1001A450</receiver_MarketParticipant.mRID>
<receiver_MarketParticipant.marketRole.type>A33</receiver_MarketParticipant.marketRole.type>
<createdDateTime>2024-10-07T14:36:40Z</createdDateTime>
<period.timeInterval>
<start>2024-10-05T22:00Z</start>
<end>2024-10-06T22:00Z</end>
</period.timeInterval>
<TimeSeries>
<mRID>1</mRID>
<auction.type>A01</auction.type>
<businessType>A62</businessType>
<in_Domain.mRID codingScheme="A01">10YBE----------2</in_Domain.mRID>
<out_Domain.mRID codingScheme="A01">10YBE----------2</out_Domain.mRID>
<contract_MarketAgreement.type>A01</contract_MarketAgreement.type>
<currency_Unit.name>EUR</currency_Unit.name>
<price_Measure_Unit.name>MWH</price_Measure_Unit.name>
<curveType>A03</curveType>
<Period>
<timeInterval>
<start>2024-10-05T22:00Z</start>
<end>2024-10-06T03:00Z</end>
</timeInterval>
<resolution>PT15M</resolution>
<Point>
<position>1</position>
<price.amount>55.35</price.amount>
</Point>
<Point>
<position>5</position>
<price.amount>44.22</price.amount>
</Point>
<Point>
<position>2</position>
<price.amount>40.32</price.amount>
</Point>
<Point>
<position>3</position>
<price.amount>31.86</price.amount>
</Point>
<Point>
<position>11</position>
<price.amount>28.37</price.amount>
</Point>
<Point>
<position>4</position>
<price.amount>28.71</price.amount>
</Point>
</Period>
<Period>
<timeInterval>
<start>2024-10-06T03:00Z</start>
<end>2024-10-06T05:00Z</end>
</timeInterval>
<resolution>PT60M</resolution>
<Point>
<position>1</position>
<price.amount>64.98</price.amount>
</Point>
<Point>
<position>3</position>
<price.amount>57.86</price.amount>
</Point>
</Period>
</TimeSeries>
</Publication_MarketDocument>
49 changes: 49 additions & 0 deletions custom_components/entsoe/test/datasets/BE_15M_exact4.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<Publication_MarketDocument xmlns="urn:iec62325.351:tc57wg16:451-3:publicationdocument:7:3">
<mRID>64e2af3a87c2404cbea80edc067a1b6f</mRID>
<revisionNumber>1</revisionNumber>
<type>A44</type>
<sender_MarketParticipant.mRID codingScheme="A01">10X1001A1001A450</sender_MarketParticipant.mRID>
<sender_MarketParticipant.marketRole.type>A32</sender_MarketParticipant.marketRole.type>
<receiver_MarketParticipant.mRID codingScheme="A01">10X1001A1001A450</receiver_MarketParticipant.mRID>
<receiver_MarketParticipant.marketRole.type>A33</receiver_MarketParticipant.marketRole.type>
<createdDateTime>2024-10-07T14:36:40Z</createdDateTime>
<period.timeInterval>
<start>2024-10-05T22:00Z</start>
<end>2024-10-06T22:00Z</end>
</period.timeInterval>
<TimeSeries>
<mRID>1</mRID>
<auction.type>A01</auction.type>
<businessType>A62</businessType>
<in_Domain.mRID codingScheme="A01">10YBE----------2</in_Domain.mRID>
<out_Domain.mRID codingScheme="A01">10YBE----------2</out_Domain.mRID>
<contract_MarketAgreement.type>A01</contract_MarketAgreement.type>
<currency_Unit.name>EUR</currency_Unit.name>
<price_Measure_Unit.name>MWH</price_Measure_Unit.name>
<curveType>A03</curveType>
<Period>
<timeInterval>
<start>2024-10-05T22:00Z</start>
<end>2024-10-05T23:00Z</end>
</timeInterval>
<resolution>PT15M</resolution>
<Point>
<position>1</position>
<price.amount>55.35</price.amount>
</Point>
<Point>
<position>2</position>
<price.amount>44.22</price.amount>
</Point>
<Point>
<position>3</position>
<price.amount>40.32</price.amount>
</Point>
<Point>
<position>4</position>
<price.amount>31.86</price.amount>
</Point>
</Period>
</TimeSeries>
</Publication_MarketDocument>
Loading

0 comments on commit f666ae5

Please sign in to comment.