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} + + + """ + + +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 == {}