diff --git a/electricitymap/contrib/parsers/CZ.py b/electricitymap/contrib/parsers/CZ.py
index 736d553207..041b15afb9 100644
--- a/electricitymap/contrib/parsers/CZ.py
+++ b/electricitymap/contrib/parsers/CZ.py
@@ -4,7 +4,7 @@
from bs4 import BeautifulSoup
# The request library is used to fetch content through HTTP
-from requests import Response, Session
+from requests import RequestException, Response, Session
from electricitymap.contrib.lib.models.event_lists import (
ExchangeList,
@@ -74,6 +74,72 @@ def get_target_datetime(dt: datetime | None) -> datetime:
return dt
+def build_payload(
+ method: str,
+ date_from: datetime,
+ date_to: datetime,
+ para1: str | None = None,
+) -> str:
+ para1_tag = f"{para1}" if para1 is not None else ""
+ return f"""
+
+
+ <{method} xmlns="https://www.ceps.cz/CepsData/">
+ {date_from.isoformat()}
+ {date_to.isoformat()}
+ QH
+ AVG
+ RT
+ {para1_tag}
+ {method}>
+
+ """
+
+
+def get_pumping_by_datetime(
+ session: Session,
+ date_from: datetime,
+ date_to: datetime,
+ zone_key: ZoneKey,
+ logger: Logger,
+) -> dict[datetime, float]:
+ """Derive pumped storage pumping per interval from the CEPS Load data,
+ as the difference between the "Load including pumping" and "Load" series.
+
+ Pumping is an optional enrichment: failures degrade to production
+ without pumped storage consumption instead of aborting the fetch.
+ """
+ try:
+ payload = build_payload("Load", date_from, date_to)
+ content = make_request(session, payload, zone_key).text
+ xml = BeautifulSoup(content, "xml")
+ mapper = get_mapper(xml) if xml.find("series") is not None else {}
+ data_tag = xml.find("data")
+
+ if (
+ data_tag is not None
+ and "Load including pumping" in mapper
+ and "Load" in mapper
+ ):
+ pumping_by_datetime: dict[datetime, float] = {}
+ for values in data_tag:
+ load_including_pumping = float(values[mapper["Load including pumping"]])
+ load = float(values[mapper["Load"]])
+ # guard against negative rounding artifacts in the source
+ pumping = max(0.0, load_including_pumping - load)
+ pumping_by_datetime[datetime.fromisoformat(values["date"])] = pumping
+ return pumping_by_datetime
+ failure = "no Load series in the response"
+ except (AssertionError, RequestException, KeyError, ValueError) as error:
+ failure = str(error)
+
+ logger.warning(
+ f"CZ.py: no usable Load data between {date_from} and {date_to} "
+ f"({failure}), pumped storage consumption will be missing"
+ )
+ return {}
+
+
def __get_exchange_data(
zone_key1: ZoneKey = ZoneKey("CZ"),
zone_key2: ZoneKey = ZoneKey("DE"),
@@ -146,30 +212,20 @@ def fetch_production(
session: Session = Session(),
target_datetime: datetime | None = None,
logger: Logger = getLogger(__name__),
-) -> list:
+) -> list[dict]:
target_datetime = get_target_datetime(target_datetime)
from_datetime = target_datetime - timedelta(hours=48)
- payload = """
-
-
-
- {}
- {}
- {}
- {}
- {}
- {}
-
-
- """.format(
- from_datetime.isoformat(), target_datetime.isoformat(), "QH", "AVG", "RT", "all"
- )
+ payload = build_payload("Generation", from_datetime, target_datetime, para1="all")
content = make_request(session, payload, zone_key).text
xml = BeautifulSoup(content, "xml")
mapper = get_mapper(xml)
+ pumping_by_datetime = get_pumping_by_datetime(
+ session, from_datetime, target_datetime, zone_key, logger
+ )
+
data_tag = xml.find("data")
production_breakdowns = ProductionBreakdownList(logger)
@@ -177,6 +233,7 @@ def fetch_production(
for values in data_tag:
production = ProductionMix()
storage = StorageMix()
+ event_datetime = datetime.fromisoformat(values["date"])
for k, v in mapper.items():
generator = translate_table_gen[k]
@@ -185,16 +242,21 @@ def fetch_production(
else:
storage.add_value(mode=generator, value=float(values[v]) * -1)
+ pumping = pumping_by_datetime.get(event_datetime)
+ if pumping is not None:
+ # net hydro storage = pumping - pumped storage generation
+ storage.add_value(mode="hydro", value=pumping)
+
production_breakdowns.append(
zoneKey=zone_key,
- datetime=datetime.fromisoformat(values["date"]),
+ datetime=event_datetime,
source=source,
production=production,
storage=storage,
)
else:
- ParserException(
+ raise ParserException(
"CZ.py",
f"There was no data returned for {zone_key} at {target_datetime}",
zone_key,
diff --git a/electricitymap/contrib/parsers/tests/__snapshots__/test_CZ/test_fetch_production.ambr b/electricitymap/contrib/parsers/tests/__snapshots__/test_CZ/test_fetch_production.ambr
new file mode 100644
index 0000000000..a8c62d1636
--- /dev/null
+++ b/electricitymap/contrib/parsers/tests/__snapshots__/test_CZ/test_fetch_production.ambr
@@ -0,0 +1,555 @@
+# serializer version: 1
+# name: test_fetch_production
+ list([
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 6, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 138.1,
+ 'coal': 3633.2,
+ 'gas': 1015.9,
+ 'hydro': 55.5,
+ 'nuclear': 4128.0,
+ 'solar': 153.4,
+ 'unknown': 0.0,
+ 'wind': 35.1,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': 0.0,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 6, 15, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 139.0,
+ 'coal': 3637.7,
+ 'gas': 1026.6,
+ 'hydro': 55.9,
+ 'nuclear': 4129.8,
+ 'solar': 220.5,
+ 'unknown': 0.0,
+ 'wind': 31.5,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': 0.0,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 6, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 138.2,
+ 'coal': 3637.3,
+ 'gas': 1043.5,
+ 'hydro': 55.9,
+ 'nuclear': 4129.0,
+ 'solar': 276.4,
+ 'unknown': 0.0,
+ 'wind': 27.2,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': -162.5,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 6, 45, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 137.8,
+ 'coal': 3643.3,
+ 'gas': 1046.9,
+ 'hydro': 55.9,
+ 'nuclear': 4128.5,
+ 'solar': 347.3,
+ 'unknown': 0.0,
+ 'wind': 21.8,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': -162.5,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 7, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 137.5,
+ 'coal': 3639.8,
+ 'gas': 1050.9,
+ 'hydro': 78.0,
+ 'nuclear': 4127.3,
+ 'solar': 421.0,
+ 'unknown': 0.0,
+ 'wind': 26.8,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': -160.1,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 7, 15, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 136.1,
+ 'coal': 3641.1,
+ 'gas': 1052.2,
+ 'hydro': 81.3,
+ 'nuclear': 4127.7,
+ 'solar': 537.9,
+ 'unknown': 0.0,
+ 'wind': 26.1,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': -166.4,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 7, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 136.1,
+ 'coal': 3651.9,
+ 'gas': 1052.2,
+ 'hydro': 81.3,
+ 'nuclear': 4128.6,
+ 'solar': 643.9,
+ 'unknown': 0.0,
+ 'wind': 32.3,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': 0.0,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 7, 45, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 136.4,
+ 'coal': 3656.7,
+ 'gas': 1047.4,
+ 'hydro': 80.2,
+ 'nuclear': 4129.6,
+ 'solar': 746.2,
+ 'unknown': 0.0,
+ 'wind': 30.5,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': 0.0,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 8, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 136.5,
+ 'coal': 3621.0,
+ 'gas': 1021.2,
+ 'hydro': 66.7,
+ 'nuclear': 4131.1,
+ 'solar': 811.3,
+ 'unknown': 0.0,
+ 'wind': 31.6,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': 0.0,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 8, 15, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 136.8,
+ 'coal': 3614.0,
+ 'gas': 1024.4,
+ 'hydro': 66.5,
+ 'nuclear': 4127.9,
+ 'solar': 914.4,
+ 'unknown': 0.0,
+ 'wind': 39.2,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': 0.0,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 8, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 136.4,
+ 'coal': 3651.8,
+ 'gas': 1018.4,
+ 'hydro': 66.5,
+ 'nuclear': 4124.9,
+ 'solar': 982.6,
+ 'unknown': 0.0,
+ 'wind': 39.7,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': 0.0,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 8, 45, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 136.5,
+ 'coal': 3684.8,
+ 'gas': 1016.3,
+ 'hydro': 64.8,
+ 'nuclear': 4121.4,
+ 'solar': 1056.0,
+ 'unknown': 0.0,
+ 'wind': 27.6,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': 0.0,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 9, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 135.6,
+ 'coal': 3658.1,
+ 'gas': 1008.6,
+ 'hydro': 44.2,
+ 'nuclear': 4118.7,
+ 'solar': 1148.7,
+ 'unknown': 0.0,
+ 'wind': 25.3,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': 0.0,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 9, 15, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 136.4,
+ 'coal': 3649.9,
+ 'gas': 980.3,
+ 'hydro': 43.7,
+ 'nuclear': 4114.5,
+ 'solar': 1176.8,
+ 'unknown': 0.0,
+ 'wind': 27.7,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': 0.0,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 9, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 136.8,
+ 'coal': 3660.9,
+ 'gas': 889.4,
+ 'hydro': 43.7,
+ 'nuclear': 4111.9,
+ 'solar': 1243.0,
+ 'unknown': 0.0,
+ 'wind': 39.8,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': 0.0,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 9, 45, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 137.4,
+ 'coal': 3663.4,
+ 'gas': 823.4,
+ 'hydro': 43.7,
+ 'nuclear': 4110.1,
+ 'solar': 1319.5,
+ 'unknown': 0.0,
+ 'wind': 38.0,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': 0.0,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 10, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 137.0,
+ 'coal': 3671.2,
+ 'gas': 820.4,
+ 'hydro': 43.7,
+ 'nuclear': 4107.8,
+ 'solar': 1408.9,
+ 'unknown': 0.0,
+ 'wind': 33.0,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': 0.0,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 10, 15, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 137.3,
+ 'coal': 3667.5,
+ 'gas': 821.2,
+ 'hydro': 43.7,
+ 'nuclear': 4107.2,
+ 'solar': 1500.9,
+ 'unknown': 0.0,
+ 'wind': 27.0,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': 0.0,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 10, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 137.7,
+ 'coal': 3668.6,
+ 'gas': 826.3,
+ 'hydro': 43.7,
+ 'nuclear': 4106.1,
+ 'solar': 1587.7,
+ 'unknown': 0.0,
+ 'wind': 26.2,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': 0.0,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 10, 45, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 138.3,
+ 'coal': 3657.7,
+ 'gas': 811.5,
+ 'hydro': 43.7,
+ 'nuclear': 4103.4,
+ 'solar': 1686.6,
+ 'unknown': 0.0,
+ 'wind': 26.0,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': 1.399,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 11, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 138.8,
+ 'coal': 3658.8,
+ 'gas': 825.3,
+ 'hydro': 43.7,
+ 'nuclear': 4102.2,
+ 'solar': 1723.9,
+ 'unknown': 0.0,
+ 'wind': 27.8,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': 104.799,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 11, 15, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 138.6,
+ 'coal': 3654.7,
+ 'gas': 825.3,
+ 'hydro': 43.7,
+ 'nuclear': 4102.2,
+ 'solar': 1774.9,
+ 'unknown': 0.0,
+ 'wind': 35.1,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': 105.2,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 11, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 138.7,
+ 'coal': 3645.4,
+ 'gas': 805.4,
+ 'hydro': 43.7,
+ 'nuclear': 4102.7,
+ 'solar': 1874.2,
+ 'unknown': 0.0,
+ 'wind': 38.8,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': 154.867,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 11, 45, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 138.6,
+ 'coal': 3643.8,
+ 'gas': 799.7,
+ 'hydro': 43.7,
+ 'nuclear': 4103.1,
+ 'solar': 1880.3,
+ 'unknown': 0.0,
+ 'wind': 40.7,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': 474.732,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ dict({
+ 'correctedModes': list([
+ ]),
+ 'datetime': datetime.datetime(2026, 7, 1, 12, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))),
+ 'end_datetime': None,
+ 'production': dict({
+ 'biomass': 137.9,
+ 'coal': 3603.2,
+ 'gas': 798.0,
+ 'hydro': 43.7,
+ 'nuclear': 4104.7,
+ 'solar': 1890.8,
+ 'unknown': 0.0,
+ 'wind': 44.2,
+ }),
+ 'source': 'ceps.cz',
+ 'sourceType': ,
+ 'storage': dict({
+ 'hydro': 487.666,
+ }),
+ 'zoneKey': 'CZ',
+ }),
+ ])
+# ---
diff --git a/electricitymap/contrib/parsers/tests/mocks/CZ/Generation.xml b/electricitymap/contrib/parsers/tests/mocks/CZ/Generation.xml
new file mode 100644
index 0000000000..44e37a8c9f
--- /dev/null
+++ b/electricitymap/contrib/parsers/tests/mocks/CZ/Generation.xml
@@ -0,0 +1 @@
+Generation2026-07-01T06:00:00+02:002026-07-01T12:00:00+02:00RTAVGQHall
\ No newline at end of file
diff --git a/electricitymap/contrib/parsers/tests/mocks/CZ/Load.xml b/electricitymap/contrib/parsers/tests/mocks/CZ/Load.xml
new file mode 100644
index 0000000000..ac9a520422
--- /dev/null
+++ b/electricitymap/contrib/parsers/tests/mocks/CZ/Load.xml
@@ -0,0 +1 @@
+Load2026-07-01T06:00:00+02:002026-07-01T12:00:00+02:00RTAVGQH
\ No newline at end of file
diff --git a/electricitymap/contrib/parsers/tests/test_CZ.py b/electricitymap/contrib/parsers/tests/test_CZ.py
index eb4fc059df..07e48dec4a 100644
--- a/electricitymap/contrib/parsers/tests/test_CZ.py
+++ b/electricitymap/contrib/parsers/tests/test_CZ.py
@@ -1,4 +1,5 @@
from datetime import datetime
+from logging import getLogger
from pathlib import Path
import pytest
@@ -14,6 +15,17 @@
# Using a fixed date that exists in the mock XML data
TARGET_DATETIME = datetime(2025, 10, 31, 18, 0, 0)
+# Fixed date matching the Generation.xml and Load.xml mocks
+PRODUCTION_TARGET_DATETIME = datetime(2026, 7, 1, 12, 0, 0)
+
+
+def _is_generation_request(request) -> bool:
+ return " bool:
+ return " tag.
+ """
+
+ requests_mock.register_uri(
+ POST,
+ CZ.url,
+ additional_matcher=_is_generation_request,
+ content=(base_path_to_mock / "Generation.xml").read_bytes(),
+ )
+ mock_load_content = (
+ ''
+ "Load"
+ ''
+ ''
+ ""
+ )
+ requests_mock.register_uri(
+ POST,
+ CZ.url,
+ additional_matcher=_is_load_request,
+ text=mock_load_content,
+ )
+
+
+def test_fetch_production(session, snapshot, mock_production_response):
+ result = CZ.fetch_production(
+ zone_key=ZoneKey("CZ"),
+ session=session,
+ target_datetime=PRODUCTION_TARGET_DATETIME,
+ )
+
+ assert snapshot(extension_class=SingleFileAmberSnapshotExtension) == result
+
+
+def test_fetch_production_storage(session, mock_production_response):
+ result = CZ.fetch_production(
+ zone_key=ZoneKey("CZ"),
+ session=session,
+ target_datetime=PRODUCTION_TARGET_DATETIME,
+ )
+
+ by_datetime = {event["datetime"].isoformat(): event for event in result}
+
+ # 06:30: pumped storage generating 162.5 MW, no pumping (both Load series equal)
+ discharging = by_datetime["2026-07-01T06:30:00+02:00"]
+ assert discharging["storage"]["hydro"] == -162.5
+
+ # 12:00: no pumped storage generation, pumping 9777.6 - 9289.934 MW
+ pumping = by_datetime["2026-07-01T12:00:00+02:00"]
+ assert pumping["storage"]["hydro"] == pytest.approx(487.666)
+
+
+def test_fetch_production_without_load_data(
+ session, mock_production_response_without_load_data
+):
+ result = CZ.fetch_production(
+ zone_key=ZoneKey("CZ"),
+ session=session,
+ target_datetime=PRODUCTION_TARGET_DATETIME,
+ )
+
+ # production events are still returned, with storage from generation only
+ assert len(result) == 25
+ assert result[2]["storage"]["hydro"] == -162.5
+
+
+def test_fetch_production_with_load_error(session, requests_mock):
+ requests_mock.register_uri(
+ POST,
+ CZ.url,
+ additional_matcher=_is_generation_request,
+ content=(base_path_to_mock / "Generation.xml").read_bytes(),
+ )
+ requests_mock.register_uri(
+ POST,
+ CZ.url,
+ additional_matcher=_is_load_request,
+ status_code=500,
+ )
+
+ result = CZ.fetch_production(
+ zone_key=ZoneKey("CZ"),
+ session=session,
+ target_datetime=PRODUCTION_TARGET_DATETIME,
+ )
+
+ # a failing Load request only costs the pumping enrichment
+ assert len(result) == 25
+ assert result[2]["storage"]["hydro"] == -162.5
+
+
+def test_fetch_production_raises_exception_on_no_data(session, requests_mock):
+ mock_xml_content = (
+ ''
+ "Generation"
+ ''
+ ""
+ )
+ requests_mock.register_uri(POST, CZ.url, text=mock_xml_content)
+
+ with pytest.raises(ParserException):
+ CZ.fetch_production(
+ zone_key=ZoneKey("CZ"),
+ session=session,
+ target_datetime=PRODUCTION_TARGET_DATETIME,
+ )
+
+
+def test_get_pumping_clamps_negative_diff(session, requests_mock):
+ mock_load_content = (
+ ''
+ "Load"
+ ''
+ ''
+ ' '
+ ' '
+ ""
+ )
+ requests_mock.register_uri(POST, CZ.url, text=mock_load_content)
+
+ result = CZ.get_pumping_by_datetime(
+ session=session,
+ date_from=datetime(2026, 7, 1, 6, 0, 0),
+ date_to=datetime(2026, 7, 1, 6, 15, 0),
+ zone_key=ZoneKey("CZ"),
+ logger=getLogger(__name__),
+ )
+
+ # negative rounding artifacts clamp to zero instead of reading as discharge
+ assert result[datetime.fromisoformat("2026-07-01T06:00:00+02:00")] == 0.0
+ assert result[datetime.fromisoformat("2026-07-01T06:15:00+02:00")] == 400.0
+
+
+def test_get_pumping_returns_empty_on_missing_series(session, requests_mock):
+ mock_load_content = (
+ ''
+ "Load"
+ ''
+ ' '
+ ""
+ )
+ requests_mock.register_uri(POST, CZ.url, text=mock_load_content)
+
+ result = CZ.get_pumping_by_datetime(
+ session=session,
+ date_from=datetime(2026, 7, 1, 6, 0, 0),
+ date_to=datetime(2026, 7, 1, 6, 15, 0),
+ zone_key=ZoneKey("CZ"),
+ logger=getLogger(__name__),
+ )
+
+ assert result == {}