Normalize RdSAP main_heating_controls mapping on the degraded 17.0 path

PRD #1385, heating-controls family. The 17.0 API mapper omitted
main_heating_controls entirely, leaving the domain field None while the
19.0/20.0/21.x paths map the first lodged control via _map_energy_element. 17.0
lodges main_heating_controls (List[EnergyElement]) in all 23 local fixtures.

The mapped element is the same EnergyElement type 17.0 already carries for
main_heating / window, so this is straight parity (not new logic). Guarded on a
non-empty list -> None, matching the siblings.

Verification: new test 2 passed; test_mapper_corpus 6002 passed; recommendation
+ heating-control tests 85 passed (the domain EnergyElement field is distinct
from the subscriptable dict HeatingControlRecommender reads via the Property
wrapper, so no recommender destabilisation); SAP-accuracy regressions pass;
pyright unchanged (39 -> 39, pre-existing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jun-te Kim 2026-07-01 12:44:02 +00:00
parent cf4c56df2e
commit 04c29cc02a
2 changed files with 59 additions and 0 deletions

View file

@ -612,6 +612,17 @@ class EpcPropertyDataMapper:
main_heating=EpcPropertyDataMapper._map_energy_elements(
schema.main_heating
),
# First control system if multiple — mirrors the 19.0/21.0.1 paths.
# 17.0 lodges main_heating_controls (List[EnergyElement]) but omitted
# it, leaving main_heating_controls=None so the heating-control
# recommendations (programmer/room-stat/TRVs) had nothing to read.
main_heating_controls=(
EpcPropertyDataMapper._map_energy_element(
schema.main_heating_controls[0]
)
if schema.main_heating_controls
else None
),
window=EpcPropertyDataMapper._map_energy_element(schema.window),
lighting=EpcPropertyDataMapper._map_energy_element(schema.lighting),
hot_water=EpcPropertyDataMapper._map_energy_element(schema.hot_water),

View file

@ -0,0 +1,48 @@
"""Regression: the RdSAP-Schema-17.0 mapper path must carry the lodged
main_heating_controls, like every other API path (19.0/20.0/21.x).
PRD #1385 (mapper normalization). 17.0 lodges `main_heating_controls`
(List[EnergyElement]) but the mapper omitted it, leaving the domain field None
while the 19.0/21.0.1 paths map the first control via `_map_energy_element`. The
mapped element is the same EnergyElement type 17.0 already carries for
`main_heating` / `window`, so this is straight parity.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
# 17.0 cert lodging a main-heating control ("Programmer, TRVs and bypass").
_FIXTURE = Path("tests/fixtures/epc_prediction/PE71NT/cert-26619d8e7f8e.json")
def _load() -> dict[str, Any]:
data: dict[str, Any] = json.loads(_FIXTURE.read_text())
assert data["schema_type"] == "RdSAP-Schema-17.0"
return data
def test_17_0_maps_lodged_main_heating_controls() -> None:
data = _load()
assert data["main_heating_controls"], "fixture must lodge a control"
epc = EpcPropertyDataMapper.from_api_response(data)
# Was None on the 17.0 path; now carries the first lodged control element.
assert epc.main_heating_controls is not None
assert epc.main_heating_controls.description == "Programmer, TRVs and bypass"
assert epc.main_heating_controls.energy_efficiency_rating == 3
def test_17_0_no_controls_maps_to_none() -> None:
# An empty control list maps to None (no phantom element), like the siblings.
data = _load()
data["main_heating_controls"] = []
epc = EpcPropertyDataMapper.from_api_response(data)
assert epc.main_heating_controls is None