mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
feat: persist solar PV arrays through the EPC round-trip 🟩
Add EpcPhotovoltaicArrayModel (epc_photovoltaic_array child table) and wire save / delete / read so sap_energy_source.photovoltaic_arrays survives load->save->load in order. Threaded through both the single get() and the bulk _for_properties() paths via _compose -> _to_energy_source. Column names match the FE migration (feature/epc-pv-and-floor-heatloss-schema). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1041c8ed0e
commit
09ba84edf9
2 changed files with 79 additions and 2 deletions
|
|
@ -10,6 +10,7 @@ from datatypes.epc.domain.epc_property_data import (
|
|||
EpcPropertyData,
|
||||
EnergyElement,
|
||||
MainHeatingDetail,
|
||||
PhotovoltaicArray,
|
||||
RenewableHeatIncentive,
|
||||
SapBuildingPart,
|
||||
SapFloorDimension,
|
||||
|
|
@ -778,6 +779,37 @@ class EpcWindowModel(SQLModel, table=True):
|
|||
)
|
||||
|
||||
|
||||
class EpcPhotovoltaicArrayModel(SQLModel, table=True):
|
||||
__tablename__: ClassVar[str] = "epc_photovoltaic_array" # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
epc_property_id: int = Field(foreign_key="epc_property.id", nullable=False)
|
||||
|
||||
# List position on `sap_energy_source.photovoltaic_arrays`. Persisted so the
|
||||
# array order is recoverable for round-trip equality (a dwelling can carry
|
||||
# several arrays at different pitch/orientation).
|
||||
array_index: int
|
||||
# kWp as a float (e.g. 3.24). pitch / overshading / orientation are SAP
|
||||
# enumeration CODES (small categorical ints), not physical degrees.
|
||||
peak_power: float
|
||||
pitch: int
|
||||
overshading: int
|
||||
orientation: Optional[int] = Field(default=None)
|
||||
|
||||
@classmethod
|
||||
def from_domain(
|
||||
cls, array: PhotovoltaicArray, array_index: int, epc_property_id: int
|
||||
) -> EpcPhotovoltaicArrayModel:
|
||||
return cls(
|
||||
epc_property_id=epc_property_id,
|
||||
array_index=array_index,
|
||||
peak_power=array.peak_power,
|
||||
pitch=array.pitch,
|
||||
overshading=array.overshading,
|
||||
orientation=array.orientation,
|
||||
)
|
||||
|
||||
|
||||
class EpcEnergyElementModel(SQLModel, table=True):
|
||||
__tablename__: ClassVar[str] = "epc_energy_element" # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from datatypes.epc.domain.epc_property_data import (
|
|||
EpcPropertyData,
|
||||
InstantaneousWwhrs,
|
||||
MainHeatingDetail,
|
||||
PhotovoltaicArray,
|
||||
PhotovoltaicSupply,
|
||||
PhotovoltaicSupplyNoneOrNoDetails,
|
||||
PvBatteries,
|
||||
|
|
@ -41,6 +42,7 @@ from infrastructure.postgres.epc_property_table import (
|
|||
EpcFlatDetailsModel,
|
||||
EpcFloorDimensionModel,
|
||||
EpcMainHeatingDetailModel,
|
||||
EpcPhotovoltaicArrayModel,
|
||||
EpcPropertyEnergyPerformanceModel,
|
||||
EpcPropertyModel,
|
||||
EpcRenewableHeatIncentiveModel,
|
||||
|
|
@ -140,6 +142,10 @@ class EpcPostgresRepository(EpcRepository):
|
|||
self._session.add(EpcFloorDimensionModel.from_domain(dim, bp_id))
|
||||
for window in data.sap_windows:
|
||||
self._session.add(EpcWindowModel.from_domain(window, epc_property_id))
|
||||
for index, array in enumerate(data.sap_energy_source.photovoltaic_arrays or []):
|
||||
self._session.add(
|
||||
EpcPhotovoltaicArrayModel.from_domain(array, index, epc_property_id)
|
||||
)
|
||||
|
||||
for element_type, elements in (
|
||||
("roof", data.roofs),
|
||||
|
|
@ -211,6 +217,7 @@ class EpcPostgresRepository(EpcRepository):
|
|||
EpcMainHeatingDetailModel,
|
||||
EpcBuildingPartModel,
|
||||
EpcWindowModel,
|
||||
EpcPhotovoltaicArrayModel,
|
||||
EpcFlatDetailsModel,
|
||||
EpcRenewableHeatIncentiveModel,
|
||||
):
|
||||
|
|
@ -327,6 +334,13 @@ class EpcPostgresRepository(EpcRepository):
|
|||
.order_by(EpcWindowModel.id) # type: ignore[arg-type]
|
||||
).all()
|
||||
)
|
||||
pv_arrays_by = _group_by_epc(
|
||||
self._session.exec(
|
||||
select(EpcPhotovoltaicArrayModel)
|
||||
.where(col(EpcPhotovoltaicArrayModel.epc_property_id).in_(epc_ids))
|
||||
.order_by(EpcPhotovoltaicArrayModel.array_index) # type: ignore[arg-type]
|
||||
).all()
|
||||
)
|
||||
part_ids = [
|
||||
bp.id
|
||||
for parts in parts_by.values()
|
||||
|
|
@ -346,6 +360,7 @@ class EpcPostgresRepository(EpcRepository):
|
|||
part_rows=parts_by.get(epc_id, []),
|
||||
floor_dims_by_part=floor_dims_by_part,
|
||||
window_rows=windows_by.get(epc_id, []),
|
||||
pv_array_rows=pv_arrays_by.get(epc_id, []),
|
||||
flat_row=flat_by.get(epc_id),
|
||||
rhi_row=rhi_by.get(epc_id),
|
||||
)
|
||||
|
|
@ -407,6 +422,7 @@ class EpcPostgresRepository(EpcRepository):
|
|||
)
|
||||
).first()
|
||||
window_rows = self._windows(epc_property_id)
|
||||
pv_array_rows = self._pv_arrays(epc_property_id)
|
||||
floor_dims_by_part = self._floor_dims_by_part(
|
||||
[bp.id for bp in part_rows if bp.id is not None]
|
||||
)
|
||||
|
|
@ -418,6 +434,7 @@ class EpcPostgresRepository(EpcRepository):
|
|||
part_rows=part_rows,
|
||||
floor_dims_by_part=floor_dims_by_part,
|
||||
window_rows=window_rows,
|
||||
pv_array_rows=pv_array_rows,
|
||||
flat_row=flat_row,
|
||||
rhi_row=rhi_row,
|
||||
)
|
||||
|
|
@ -432,6 +449,7 @@ class EpcPostgresRepository(EpcRepository):
|
|||
part_rows: list[EpcBuildingPartModel],
|
||||
floor_dims_by_part: dict[int, list[EpcFloorDimensionModel]],
|
||||
window_rows: list[EpcWindowModel],
|
||||
pv_array_rows: list[EpcPhotovoltaicArrayModel],
|
||||
flat_row: Optional[EpcFlatDetailsModel],
|
||||
rhi_row: Optional[EpcRenewableHeatIncentiveModel],
|
||||
) -> EpcPropertyData:
|
||||
|
|
@ -457,7 +475,7 @@ class EpcPostgresRepository(EpcRepository):
|
|||
door_count=p.door_count,
|
||||
sap_heating=self._to_sap_heating(p, heating_rows),
|
||||
sap_windows=[self._to_window(w) for w in window_rows],
|
||||
sap_energy_source=self._to_energy_source(p),
|
||||
sap_energy_source=self._to_energy_source(p, pv_array_rows),
|
||||
sap_building_parts=[
|
||||
self._to_building_part(
|
||||
bp, floor_dims_by_part.get(bp.id, []) if bp.id is not None else []
|
||||
|
|
@ -622,6 +640,18 @@ class EpcPostgresRepository(EpcRepository):
|
|||
).all()
|
||||
)
|
||||
|
||||
@private
|
||||
def _pv_arrays(
|
||||
self, epc_property_id: int
|
||||
) -> list[EpcPhotovoltaicArrayModel]:
|
||||
return list(
|
||||
self._session.exec(
|
||||
select(EpcPhotovoltaicArrayModel)
|
||||
.where(EpcPhotovoltaicArrayModel.epc_property_id == epc_property_id)
|
||||
.order_by(EpcPhotovoltaicArrayModel.array_index) # type: ignore[arg-type]
|
||||
).all()
|
||||
)
|
||||
|
||||
@private
|
||||
def _to_energy_element(self, e: EpcEnergyElementModel) -> EnergyElement:
|
||||
return EnergyElement(
|
||||
|
|
@ -817,8 +847,23 @@ class EpcPostgresRepository(EpcRepository):
|
|||
)
|
||||
|
||||
@private
|
||||
def _to_energy_source(self, p: EpcPropertyModel) -> SapEnergySource:
|
||||
def _to_energy_source(
|
||||
self, p: EpcPropertyModel, pv_array_rows: list[EpcPhotovoltaicArrayModel]
|
||||
) -> SapEnergySource:
|
||||
return SapEnergySource(
|
||||
photovoltaic_arrays=(
|
||||
[
|
||||
PhotovoltaicArray(
|
||||
peak_power=a.peak_power,
|
||||
pitch=a.pitch,
|
||||
overshading=a.overshading,
|
||||
orientation=a.orientation,
|
||||
)
|
||||
for a in sorted(pv_array_rows, key=lambda a: a.array_index)
|
||||
]
|
||||
if pv_array_rows
|
||||
else None
|
||||
),
|
||||
mains_gas=p.energy_mains_gas,
|
||||
meter_type=p.energy_meter_type,
|
||||
pv_battery_count=p.energy_pv_battery_count,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue