mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +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,
|
EpcPropertyData,
|
||||||
EnergyElement,
|
EnergyElement,
|
||||||
MainHeatingDetail,
|
MainHeatingDetail,
|
||||||
|
PhotovoltaicArray,
|
||||||
RenewableHeatIncentive,
|
RenewableHeatIncentive,
|
||||||
SapBuildingPart,
|
SapBuildingPart,
|
||||||
SapFloorDimension,
|
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):
|
class EpcEnergyElementModel(SQLModel, table=True):
|
||||||
__tablename__: ClassVar[str] = "epc_energy_element" # pyright: ignore[reportIncompatibleVariableOverride]
|
__tablename__: ClassVar[str] = "epc_energy_element" # pyright: ignore[reportIncompatibleVariableOverride]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ from datatypes.epc.domain.epc_property_data import (
|
||||||
EpcPropertyData,
|
EpcPropertyData,
|
||||||
InstantaneousWwhrs,
|
InstantaneousWwhrs,
|
||||||
MainHeatingDetail,
|
MainHeatingDetail,
|
||||||
|
PhotovoltaicArray,
|
||||||
PhotovoltaicSupply,
|
PhotovoltaicSupply,
|
||||||
PhotovoltaicSupplyNoneOrNoDetails,
|
PhotovoltaicSupplyNoneOrNoDetails,
|
||||||
PvBatteries,
|
PvBatteries,
|
||||||
|
|
@ -41,6 +42,7 @@ from infrastructure.postgres.epc_property_table import (
|
||||||
EpcFlatDetailsModel,
|
EpcFlatDetailsModel,
|
||||||
EpcFloorDimensionModel,
|
EpcFloorDimensionModel,
|
||||||
EpcMainHeatingDetailModel,
|
EpcMainHeatingDetailModel,
|
||||||
|
EpcPhotovoltaicArrayModel,
|
||||||
EpcPropertyEnergyPerformanceModel,
|
EpcPropertyEnergyPerformanceModel,
|
||||||
EpcPropertyModel,
|
EpcPropertyModel,
|
||||||
EpcRenewableHeatIncentiveModel,
|
EpcRenewableHeatIncentiveModel,
|
||||||
|
|
@ -140,6 +142,10 @@ class EpcPostgresRepository(EpcRepository):
|
||||||
self._session.add(EpcFloorDimensionModel.from_domain(dim, bp_id))
|
self._session.add(EpcFloorDimensionModel.from_domain(dim, bp_id))
|
||||||
for window in data.sap_windows:
|
for window in data.sap_windows:
|
||||||
self._session.add(EpcWindowModel.from_domain(window, epc_property_id))
|
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 (
|
for element_type, elements in (
|
||||||
("roof", data.roofs),
|
("roof", data.roofs),
|
||||||
|
|
@ -211,6 +217,7 @@ class EpcPostgresRepository(EpcRepository):
|
||||||
EpcMainHeatingDetailModel,
|
EpcMainHeatingDetailModel,
|
||||||
EpcBuildingPartModel,
|
EpcBuildingPartModel,
|
||||||
EpcWindowModel,
|
EpcWindowModel,
|
||||||
|
EpcPhotovoltaicArrayModel,
|
||||||
EpcFlatDetailsModel,
|
EpcFlatDetailsModel,
|
||||||
EpcRenewableHeatIncentiveModel,
|
EpcRenewableHeatIncentiveModel,
|
||||||
):
|
):
|
||||||
|
|
@ -327,6 +334,13 @@ class EpcPostgresRepository(EpcRepository):
|
||||||
.order_by(EpcWindowModel.id) # type: ignore[arg-type]
|
.order_by(EpcWindowModel.id) # type: ignore[arg-type]
|
||||||
).all()
|
).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 = [
|
part_ids = [
|
||||||
bp.id
|
bp.id
|
||||||
for parts in parts_by.values()
|
for parts in parts_by.values()
|
||||||
|
|
@ -346,6 +360,7 @@ class EpcPostgresRepository(EpcRepository):
|
||||||
part_rows=parts_by.get(epc_id, []),
|
part_rows=parts_by.get(epc_id, []),
|
||||||
floor_dims_by_part=floor_dims_by_part,
|
floor_dims_by_part=floor_dims_by_part,
|
||||||
window_rows=windows_by.get(epc_id, []),
|
window_rows=windows_by.get(epc_id, []),
|
||||||
|
pv_array_rows=pv_arrays_by.get(epc_id, []),
|
||||||
flat_row=flat_by.get(epc_id),
|
flat_row=flat_by.get(epc_id),
|
||||||
rhi_row=rhi_by.get(epc_id),
|
rhi_row=rhi_by.get(epc_id),
|
||||||
)
|
)
|
||||||
|
|
@ -407,6 +422,7 @@ class EpcPostgresRepository(EpcRepository):
|
||||||
)
|
)
|
||||||
).first()
|
).first()
|
||||||
window_rows = self._windows(epc_property_id)
|
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(
|
floor_dims_by_part = self._floor_dims_by_part(
|
||||||
[bp.id for bp in part_rows if bp.id is not None]
|
[bp.id for bp in part_rows if bp.id is not None]
|
||||||
)
|
)
|
||||||
|
|
@ -418,6 +434,7 @@ class EpcPostgresRepository(EpcRepository):
|
||||||
part_rows=part_rows,
|
part_rows=part_rows,
|
||||||
floor_dims_by_part=floor_dims_by_part,
|
floor_dims_by_part=floor_dims_by_part,
|
||||||
window_rows=window_rows,
|
window_rows=window_rows,
|
||||||
|
pv_array_rows=pv_array_rows,
|
||||||
flat_row=flat_row,
|
flat_row=flat_row,
|
||||||
rhi_row=rhi_row,
|
rhi_row=rhi_row,
|
||||||
)
|
)
|
||||||
|
|
@ -432,6 +449,7 @@ class EpcPostgresRepository(EpcRepository):
|
||||||
part_rows: list[EpcBuildingPartModel],
|
part_rows: list[EpcBuildingPartModel],
|
||||||
floor_dims_by_part: dict[int, list[EpcFloorDimensionModel]],
|
floor_dims_by_part: dict[int, list[EpcFloorDimensionModel]],
|
||||||
window_rows: list[EpcWindowModel],
|
window_rows: list[EpcWindowModel],
|
||||||
|
pv_array_rows: list[EpcPhotovoltaicArrayModel],
|
||||||
flat_row: Optional[EpcFlatDetailsModel],
|
flat_row: Optional[EpcFlatDetailsModel],
|
||||||
rhi_row: Optional[EpcRenewableHeatIncentiveModel],
|
rhi_row: Optional[EpcRenewableHeatIncentiveModel],
|
||||||
) -> EpcPropertyData:
|
) -> EpcPropertyData:
|
||||||
|
|
@ -457,7 +475,7 @@ class EpcPostgresRepository(EpcRepository):
|
||||||
door_count=p.door_count,
|
door_count=p.door_count,
|
||||||
sap_heating=self._to_sap_heating(p, heating_rows),
|
sap_heating=self._to_sap_heating(p, heating_rows),
|
||||||
sap_windows=[self._to_window(w) for w in window_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=[
|
sap_building_parts=[
|
||||||
self._to_building_part(
|
self._to_building_part(
|
||||||
bp, floor_dims_by_part.get(bp.id, []) if bp.id is not None else []
|
bp, floor_dims_by_part.get(bp.id, []) if bp.id is not None else []
|
||||||
|
|
@ -622,6 +640,18 @@ class EpcPostgresRepository(EpcRepository):
|
||||||
).all()
|
).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
|
@private
|
||||||
def _to_energy_element(self, e: EpcEnergyElementModel) -> EnergyElement:
|
def _to_energy_element(self, e: EpcEnergyElementModel) -> EnergyElement:
|
||||||
return EnergyElement(
|
return EnergyElement(
|
||||||
|
|
@ -817,8 +847,23 @@ class EpcPostgresRepository(EpcRepository):
|
||||||
)
|
)
|
||||||
|
|
||||||
@private
|
@private
|
||||||
def _to_energy_source(self, p: EpcPropertyModel) -> SapEnergySource:
|
def _to_energy_source(
|
||||||
|
self, p: EpcPropertyModel, pv_array_rows: list[EpcPhotovoltaicArrayModel]
|
||||||
|
) -> SapEnergySource:
|
||||||
return 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,
|
mains_gas=p.energy_mains_gas,
|
||||||
meter_type=p.energy_meter_type,
|
meter_type=p.energy_meter_type,
|
||||||
pv_battery_count=p.energy_pv_battery_count,
|
pv_battery_count=p.energy_pv_battery_count,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue