diff --git a/infrastructure/postgres/epc_property_table.py b/infrastructure/postgres/epc_property_table.py index a9ab5108..74443e7a 100644 --- a/infrastructure/postgres/epc_property_table.py +++ b/infrastructure/postgres/epc_property_table.py @@ -139,6 +139,9 @@ class EpcPropertyModel(SQLModel, table=True): ) energy_pv_percent_roof_area: Optional[int] = Field(default=None) energy_pv_battery_capacity: Optional[float] = Field(default=None) + # An EV/PV diverter present on the dwelling (SAP §M; the calculator reads it + # via cert_to_inputs). Non-optional bool defaulting False, matching the domain. + energy_pv_diverter_present: bool = Field(default=False) energy_wind_turbine_hub_height: Optional[float] = Field(default=None) energy_wind_turbine_rotor_diameter: Optional[float] = Field(default=None) @@ -162,6 +165,7 @@ class EpcPropertyModel(SQLModel, table=True): default=None, sa_column=Column(JSONB, nullable=True) ) heating_cylinder_insulation_thickness_mm: Optional[int] = Field(default=None) + heating_cylinder_volume_measured_l: Optional[int] = Field(default=None) heating_wwhrs_index_number_1: Optional[int] = Field(default=None) heating_wwhrs_index_number_2: Optional[int] = Field(default=None) heating_shower_outlet_type: Optional[Union[int, str]] = Field( @@ -193,6 +197,7 @@ class EpcPropertyModel(SQLModel, table=True): ventilation_suspended_timber_floor_sealed: Optional[bool] = Field(default=None) ventilation_has_draught_lobby: Optional[bool] = Field(default=None) ventilation_air_permeability_ap4_m3_h_m2: Optional[float] = Field(default=None) + ventilation_air_permeability_ap50_m3_h_m2: Optional[float] = Field(default=None) ventilation_mechanical_ventilation_kind: Optional[str] = Field(default=None) mechanical_ventilation: Optional[int] = Field(default=None) mechanical_vent_duct_type: Optional[int] = Field(default=None) @@ -341,6 +346,7 @@ class EpcPropertyModel(SQLModel, table=True): pv.none_or_no_details.percent_roof_area if pv else None ), energy_pv_battery_capacity=pvb.pv_battery.battery_capacity if pvb else None, + energy_pv_diverter_present=es.pv_diverter_present, energy_wind_turbine_hub_height=wt.hub_height if wt else None, energy_wind_turbine_rotor_diameter=wt.rotor_diameter if wt else None, heating_cylinder_size=h.cylinder_size, @@ -352,6 +358,7 @@ class EpcPropertyModel(SQLModel, table=True): heating_secondary_fuel_type=h.secondary_fuel_type, heating_secondary_heating_type=h.secondary_heating_type, heating_cylinder_insulation_thickness_mm=h.cylinder_insulation_thickness_mm, + heating_cylinder_volume_measured_l=h.cylinder_volume_measured_l, heating_wwhrs_index_number_1=h.instantaneous_wwhrs.wwhrs_index_number1, heating_wwhrs_index_number_2=h.instantaneous_wwhrs.wwhrs_index_number2, heating_shower_outlet_type=shower.shower_outlet_type if shower else None, @@ -385,6 +392,9 @@ class EpcPropertyModel(SQLModel, table=True): ventilation_air_permeability_ap4_m3_h_m2=( v.air_permeability_ap4_m3_h_m2 if v else None ), + ventilation_air_permeability_ap50_m3_h_m2=( + v.air_permeability_ap50_m3_h_m2 if v else None + ), ventilation_mechanical_ventilation_kind=( v.mechanical_ventilation_kind if v else None ), @@ -544,6 +554,10 @@ class EpcMainHeatingDetailModel(SQLModel, table=True): main_heating_data_source: Optional[int] = Field(default=None) condensing: Optional[bool] = Field(default=None) weather_compensator: Optional[bool] = Field(default=None) + # Community-heating fields (SAP Table 4d; the calculator reads them via + # cert_to_inputs for a community-heated dwelling). + community_heating_boiler_fuel_type: Optional[int] = Field(default=None) + community_heating_chp_fraction: Optional[float] = Field(default=None) @classmethod def from_domain( @@ -569,6 +583,8 @@ class EpcMainHeatingDetailModel(SQLModel, table=True): main_heating_data_source=detail.main_heating_data_source, condensing=detail.condensing, weather_compensator=detail.weather_compensator, + community_heating_boiler_fuel_type=detail.community_heating_boiler_fuel_type, + community_heating_chp_fraction=detail.community_heating_chp_fraction, ) @@ -601,6 +617,11 @@ class EpcBuildingPartModel(SQLModel, table=True): wall_insulation_thickness: Optional[Union[str, int]] = Field( default=None, sa_column=Column(JSONB, nullable=True) ) + # Union[int, str] SAP code (int from the API, str from Site Notes) — JSONB to + # preserve the type on round-trip, like the insulation-thickness siblings. + wall_insulation_thermal_conductivity: Optional[Union[int, str]] = Field( + default=None, sa_column=Column(JSONB, nullable=True) + ) floor_heat_loss: Optional[int] = Field(default=None) floor_insulation_thickness: Optional[str] = Field(default=None) flat_roof_insulation_thickness: Optional[Union[str, int]] = Field( @@ -627,12 +648,14 @@ class EpcBuildingPartModel(SQLModel, table=True): alt_wall_1_insulation_type: Optional[int] = Field(default=None) alt_wall_1_thickness_measured: Optional[str] = Field(default=None) alt_wall_1_insulation_thickness: Optional[str] = Field(default=None) + alt_wall_1_is_sheltered: Optional[bool] = Field(default=None) alt_wall_2_area: Optional[float] = Field(default=None) alt_wall_2_dry_lined: Optional[str] = Field(default=None) alt_wall_2_construction: Optional[int] = Field(default=None) alt_wall_2_insulation_type: Optional[int] = Field(default=None) alt_wall_2_thickness_measured: Optional[str] = Field(default=None) alt_wall_2_insulation_thickness: Optional[str] = Field(default=None) + alt_wall_2_is_sheltered: Optional[bool] = Field(default=None) @classmethod def from_domain( @@ -653,6 +676,7 @@ class EpcBuildingPartModel(SQLModel, table=True): wall_dry_lined=part.wall_dry_lined, wall_thickness_mm=part.wall_thickness_mm, wall_insulation_thickness=part.wall_insulation_thickness, + wall_insulation_thermal_conductivity=part.wall_insulation_thermal_conductivity, floor_heat_loss=part.floor_heat_loss, floor_insulation_thickness=part.floor_insulation_thickness, flat_roof_insulation_thickness=part.flat_roof_insulation_thickness, @@ -677,6 +701,7 @@ class EpcBuildingPartModel(SQLModel, table=True): alt_wall_1_insulation_thickness=( aw1.wall_insulation_thickness if aw1 else None ), + alt_wall_1_is_sheltered=aw1.is_sheltered if aw1 else None, alt_wall_2_area=aw2.wall_area if aw2 else None, alt_wall_2_dry_lined=aw2.wall_dry_lined if aw2 else None, alt_wall_2_construction=aw2.wall_construction if aw2 else None, @@ -685,6 +710,7 @@ class EpcBuildingPartModel(SQLModel, table=True): alt_wall_2_insulation_thickness=( aw2.wall_insulation_thickness if aw2 else None ), + alt_wall_2_is_sheltered=aw2.is_sheltered if aw2 else None, ) diff --git a/repositories/epc/epc_postgres_repository.py b/repositories/epc/epc_postgres_repository.py index ffd746af..3f4e09b1 100644 --- a/repositories/epc/epc_postgres_repository.py +++ b/repositories/epc/epc_postgres_repository.py @@ -691,6 +691,7 @@ class EpcPostgresRepository(EpcRepository): secondary_fuel_type=p.heating_secondary_fuel_type, secondary_heating_type=p.heating_secondary_heating_type, cylinder_insulation_thickness_mm=p.heating_cylinder_insulation_thickness_mm, + cylinder_volume_measured_l=p.heating_cylinder_volume_measured_l, number_baths=p.heating_number_baths, number_baths_wwhrs=p.heating_number_baths_wwhrs, electric_shower_count=p.heating_electric_shower_count, @@ -718,6 +719,8 @@ class EpcPostgresRepository(EpcRepository): main_heating_data_source=m.main_heating_data_source, condensing=m.condensing, weather_compensator=m.weather_compensator, + community_heating_boiler_fuel_type=m.community_heating_boiler_fuel_type, + community_heating_chp_fraction=m.community_heating_chp_fraction, ) @private @@ -768,6 +771,7 @@ class EpcPostgresRepository(EpcRepository): wall_dry_lined=bp.wall_dry_lined, wall_thickness_mm=bp.wall_thickness_mm, wall_insulation_thickness=bp.wall_insulation_thickness, + wall_insulation_thermal_conductivity=bp.wall_insulation_thermal_conductivity, sap_alternative_wall_1=self._to_alt_wall(bp, 1), sap_alternative_wall_2=self._to_alt_wall(bp, 2), floor_heat_loss=bp.floor_heat_loss, @@ -819,6 +823,7 @@ class EpcPostgresRepository(EpcRepository): if n == 1 else bp.alt_wall_2_insulation_thickness ) + sheltered = bp.alt_wall_1_is_sheltered if n == 1 else bp.alt_wall_2_is_sheltered return SapAlternativeWall( wall_area=area, wall_dry_lined=_require(dry_lined, f"alt_wall_{n}_dry_lined"), @@ -830,6 +835,9 @@ class EpcPostgresRepository(EpcRepository): thickness_measured, f"alt_wall_{n}_thickness_measured" ), wall_insulation_thickness=insulation_thickness, + # Nullable column (added later than the alt wall itself); a row from + # before the column existed reads None → the domain default False. + is_sheltered=sheltered if sheltered is not None else False, ) @private @@ -864,6 +872,7 @@ class EpcPostgresRepository(EpcRepository): if pv_array_rows else None ), + pv_diverter_present=p.energy_pv_diverter_present, mains_gas=p.energy_mains_gas, meter_type=p.energy_meter_type, pv_battery_count=p.energy_pv_battery_count, @@ -949,6 +958,7 @@ class EpcPostgresRepository(EpcRepository): suspended_timber_floor_sealed=p.ventilation_suspended_timber_floor_sealed, has_draught_lobby=p.ventilation_has_draught_lobby, air_permeability_ap4_m3_h_m2=p.ventilation_air_permeability_ap4_m3_h_m2, + air_permeability_ap50_m3_h_m2=p.ventilation_air_permeability_ap50_m3_h_m2, mechanical_ventilation_kind=p.ventilation_mechanical_ventilation_kind, )