mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
Map 20.0.0 lighting, ventilation and hot-water demand fields 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3352f11be3
commit
eb5bb89612
2 changed files with 137 additions and 2 deletions
|
|
@ -1077,6 +1077,8 @@ class EpcPropertyDataMapper:
|
|||
@staticmethod
|
||||
def from_rdsap_schema_20_0_0(schema: RdSapSchema20_0_0) -> EpcPropertyData:
|
||||
es = schema.sap_energy_source
|
||||
# ADR-0027: instantaneous_wwhrs holds bath/shower ROOM counts.
|
||||
iw = schema.sap_heating.instantaneous_wwhrs
|
||||
return EpcPropertyData(
|
||||
uprn=schema.uprn,
|
||||
assessment_type=schema.assessment_type,
|
||||
|
|
@ -1110,12 +1112,27 @@ class EpcPropertyDataMapper:
|
|||
heated_rooms_count=schema.heated_room_count,
|
||||
wet_rooms_count=0,
|
||||
extensions_count=schema.extensions_count,
|
||||
open_chimneys_count=0,
|
||||
open_chimneys_count=schema.open_fireplaces_count,
|
||||
insulated_door_count=schema.insulated_door_count,
|
||||
draughtproofed_door_count=None,
|
||||
percent_draughtproofed=schema.percent_draughtproofed,
|
||||
# ADR-0027: 20.0.0 has no flue/fan/vent counts (calculator defaults
|
||||
# them via RdSAP Table 5), but sheltered_sides must come from
|
||||
# built_form — else the calculator assumes mid-terrace (2) for all.
|
||||
sap_ventilation=SapVentilation(
|
||||
sheltered_sides=_api_sheltered_sides(schema.built_form),
|
||||
),
|
||||
# ADR-0027: 20.0.0 gives total + low-energy OUTLET counts, not an
|
||||
# LED/CFL/incandescent split. Low-energy → the calculator's LEL
|
||||
# path (unknown LED/CFL split); the remainder is incandescent (1
|
||||
# outlet ≈ 1 bulb). Fixes a 439-cert lighting understatement.
|
||||
led_fixed_lighting_bulbs_count=0,
|
||||
cfl_fixed_lighting_bulbs_count=0,
|
||||
incandescent_fixed_lighting_bulbs_count=0,
|
||||
incandescent_fixed_lighting_bulbs_count=(
|
||||
schema.fixed_lighting_outlets_count
|
||||
- schema.low_energy_fixed_lighting_outlets_count
|
||||
),
|
||||
low_energy_fixed_lighting_bulbs_count=schema.low_energy_fixed_lighting_outlets_count,
|
||||
roofs=EpcPropertyDataMapper._map_energy_elements(schema.roofs),
|
||||
walls=EpcPropertyDataMapper._map_energy_elements(schema.walls),
|
||||
floors=EpcPropertyDataMapper._map_energy_elements(schema.floors),
|
||||
|
|
@ -1131,6 +1148,20 @@ class EpcPropertyDataMapper:
|
|||
sap_heating=SapHeating(
|
||||
# 20.0.0 uses room counts not product index numbers; domain fields default to None
|
||||
instantaneous_wwhrs=InstantaneousWwhrs(),
|
||||
# ADR-0027: derive HW demand counts from the bath/shower ROOM
|
||||
# counts (instantaneous_wwhrs is the false-friend container).
|
||||
number_baths=(
|
||||
iw.rooms_with_bath_and_or_shower
|
||||
+ iw.rooms_with_bath_and_mixer_shower
|
||||
if iw is not None
|
||||
else None
|
||||
),
|
||||
mixer_shower_count=(
|
||||
iw.rooms_with_mixer_shower_no_bath
|
||||
+ iw.rooms_with_bath_and_mixer_shower
|
||||
if iw is not None
|
||||
else None
|
||||
),
|
||||
main_heating_details=[
|
||||
MainHeatingDetail(
|
||||
has_fghrs=d.has_fghrs == "Y",
|
||||
|
|
|
|||
|
|
@ -1261,3 +1261,107 @@ class TestRdSap20_0_0ReducedFieldSynthesis:
|
|||
|
||||
# Assert — cascade remaps 1 ("DG pre-2002") -> 2 (double), not raw 1.
|
||||
assert all(w.glazing_type == 2 for w in result.sap_windows)
|
||||
|
||||
def test_lighting_counts_incandescent_remainder_and_low_energy_as_lel(
|
||||
self,
|
||||
) -> None:
|
||||
# Arrange — ADR-0027: 20.0.0 gives total + low-energy OUTLET counts, not
|
||||
# an LED/CFL/incandescent split. The non-low-energy remainder is
|
||||
# incandescent (else lighting energy is understated for the 439/1000
|
||||
# certs that have any); low-energy → the calculator's LEL path (unknown
|
||||
# LED/CFL split). A cert with some incandescent outlets.
|
||||
corpus = _load_20_0_0_corpus()
|
||||
if not corpus:
|
||||
pytest.skip("no RdSAP-Schema-20.0.0 corpus harvested")
|
||||
cert = next(
|
||||
(
|
||||
c
|
||||
for c in corpus
|
||||
if not c.get("sap_windows")
|
||||
and (c.get("fixed_lighting_outlets_count") or 0)
|
||||
> (c.get("low_energy_fixed_lighting_outlets_count") or 0)
|
||||
),
|
||||
None,
|
||||
)
|
||||
if cert is None:
|
||||
pytest.skip("no corpus cert with incandescent lighting")
|
||||
total = cert["fixed_lighting_outlets_count"]
|
||||
low = cert["low_energy_fixed_lighting_outlets_count"]
|
||||
|
||||
# Act
|
||||
result = EpcPropertyDataMapper.from_api_response(cert)
|
||||
|
||||
# Assert
|
||||
assert result.incandescent_fixed_lighting_bulbs_count == total - low
|
||||
assert result.low_energy_fixed_lighting_bulbs_count == low
|
||||
assert result.led_fixed_lighting_bulbs_count == 0
|
||||
assert result.cfl_fixed_lighting_bulbs_count == 0
|
||||
|
||||
def test_ventilation_maps_chimneys_draughtproofing_and_sheltered_sides(
|
||||
self,
|
||||
) -> None:
|
||||
# Arrange — ADR-0027: 20.0.0 lodges open_fireplaces_count (currently
|
||||
# dropped → -80 m³/h/chimney for 53 certs), percent_draughtproofed, and
|
||||
# built_form. Build sap_ventilation with sheltered_sides from built_form
|
||||
# (else the calculator defaults every dwelling to mid-terrace=2). A cert
|
||||
# with an open fireplace.
|
||||
from datatypes.epc.domain.mapper import _api_sheltered_sides # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
corpus = _load_20_0_0_corpus()
|
||||
if not corpus:
|
||||
pytest.skip("no RdSAP-Schema-20.0.0 corpus harvested")
|
||||
cert = next(
|
||||
(
|
||||
c
|
||||
for c in corpus
|
||||
if not c.get("sap_windows") and (c.get("open_fireplaces_count") or 0) >= 1
|
||||
),
|
||||
None,
|
||||
)
|
||||
if cert is None:
|
||||
pytest.skip("no corpus cert with an open fireplace")
|
||||
|
||||
# Act
|
||||
result = EpcPropertyDataMapper.from_api_response(cert)
|
||||
|
||||
# Assert
|
||||
assert result.open_chimneys_count == cert["open_fireplaces_count"]
|
||||
assert result.percent_draughtproofed == cert["percent_draughtproofed"]
|
||||
assert result.sap_ventilation is not None
|
||||
assert result.sap_ventilation.sheltered_sides == _api_sheltered_sides(
|
||||
cert["built_form"]
|
||||
)
|
||||
|
||||
def test_hot_water_derives_bath_and_mixer_counts_from_room_counts(self) -> None:
|
||||
# Arrange — ADR-0027: 20.0.0's instantaneous_wwhrs carries bath/shower
|
||||
# ROOM counts (a false-friend for the WWHR device index). Derive
|
||||
# number_baths and mixer_shower_count from them so HW demand isn't pinned
|
||||
# to the calculator's modal 1-bath default (496/1000 have ≠1 bath).
|
||||
corpus = _load_20_0_0_corpus()
|
||||
if not corpus:
|
||||
pytest.skip("no RdSAP-Schema-20.0.0 corpus harvested")
|
||||
cert = next(
|
||||
(
|
||||
c
|
||||
for c in corpus
|
||||
if not c.get("sap_windows")
|
||||
and c.get("sap_heating", {}).get("instantaneous_wwhrs")
|
||||
),
|
||||
None,
|
||||
)
|
||||
if cert is None:
|
||||
pytest.skip("no corpus cert with instantaneous_wwhrs")
|
||||
iw = cert["sap_heating"]["instantaneous_wwhrs"]
|
||||
expected_baths = iw["rooms_with_bath_and_or_shower"] + iw[
|
||||
"rooms_with_bath_and_mixer_shower"
|
||||
]
|
||||
expected_mixers = iw["rooms_with_mixer_shower_no_bath"] + iw[
|
||||
"rooms_with_bath_and_mixer_shower"
|
||||
]
|
||||
|
||||
# Act
|
||||
result = EpcPropertyDataMapper.from_api_response(cert)
|
||||
|
||||
# Assert
|
||||
assert result.sap_heating.number_baths == expected_baths
|
||||
assert result.sap_heating.mixer_shower_count == expected_mixers
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue