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
|
@staticmethod
|
||||||
def from_rdsap_schema_20_0_0(schema: RdSapSchema20_0_0) -> EpcPropertyData:
|
def from_rdsap_schema_20_0_0(schema: RdSapSchema20_0_0) -> EpcPropertyData:
|
||||||
es = schema.sap_energy_source
|
es = schema.sap_energy_source
|
||||||
|
# ADR-0027: instantaneous_wwhrs holds bath/shower ROOM counts.
|
||||||
|
iw = schema.sap_heating.instantaneous_wwhrs
|
||||||
return EpcPropertyData(
|
return EpcPropertyData(
|
||||||
uprn=schema.uprn,
|
uprn=schema.uprn,
|
||||||
assessment_type=schema.assessment_type,
|
assessment_type=schema.assessment_type,
|
||||||
|
|
@ -1110,12 +1112,27 @@ class EpcPropertyDataMapper:
|
||||||
heated_rooms_count=schema.heated_room_count,
|
heated_rooms_count=schema.heated_room_count,
|
||||||
wet_rooms_count=0,
|
wet_rooms_count=0,
|
||||||
extensions_count=schema.extensions_count,
|
extensions_count=schema.extensions_count,
|
||||||
open_chimneys_count=0,
|
open_chimneys_count=schema.open_fireplaces_count,
|
||||||
insulated_door_count=schema.insulated_door_count,
|
insulated_door_count=schema.insulated_door_count,
|
||||||
draughtproofed_door_count=None,
|
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,
|
led_fixed_lighting_bulbs_count=0,
|
||||||
cfl_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),
|
roofs=EpcPropertyDataMapper._map_energy_elements(schema.roofs),
|
||||||
walls=EpcPropertyDataMapper._map_energy_elements(schema.walls),
|
walls=EpcPropertyDataMapper._map_energy_elements(schema.walls),
|
||||||
floors=EpcPropertyDataMapper._map_energy_elements(schema.floors),
|
floors=EpcPropertyDataMapper._map_energy_elements(schema.floors),
|
||||||
|
|
@ -1131,6 +1148,20 @@ class EpcPropertyDataMapper:
|
||||||
sap_heating=SapHeating(
|
sap_heating=SapHeating(
|
||||||
# 20.0.0 uses room counts not product index numbers; domain fields default to None
|
# 20.0.0 uses room counts not product index numbers; domain fields default to None
|
||||||
instantaneous_wwhrs=InstantaneousWwhrs(),
|
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=[
|
main_heating_details=[
|
||||||
MainHeatingDetail(
|
MainHeatingDetail(
|
||||||
has_fghrs=d.has_fghrs == "Y",
|
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 — cascade remaps 1 ("DG pre-2002") -> 2 (double), not raw 1.
|
||||||
assert all(w.glazing_type == 2 for w in result.sap_windows)
|
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