SolarPotential carries panel dims + per-segment centre/area 🟩

Enrich each SolarRoofSegment from roofSegmentStats (centre + areaMeters2, keyed
by segmentIndex) and read panelHeightMeters/panelWidthMeters onto SolarPotential
— the geometry the Dwelling-Roof Cap (ADR-0038) needs. All Optional; existing
projection + config-selection tests stay green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-26 12:14:00 +00:00
parent cf2780ed77
commit d6a59be950

View file

@ -62,6 +62,14 @@ class SolarRoofSegment:
azimuth_degrees: float
pitch_degrees: float
yearly_energy_dc_kwh: float
# Per-segment centre + roof-plane area, enriched from the top-level
# `roofSegmentStats` (keyed by `segmentIndex`) — the per-config
# `roofSegmentSummaries` omit them. Used by the Dwelling-Roof Cap
# (ADR-0038) to rank segments by distance from the dwelling and bound the
# array by usable roof area. None when the stats block lacks the segment.
center_latitude: Optional[float] = None
center_longitude: Optional[float] = None
area_m2: Optional[float] = None
@property
def sap_orientation(self) -> int:
@ -94,6 +102,11 @@ class SolarPotential:
panel_capacity_watts: float
max_array_panels_count: int
configurations: tuple[SolarPanelConfiguration, ...]
# Physical panel footprint (Google `panelHeightMeters` / `panelWidthMeters`)
# — the Dwelling-Roof Cap (ADR-0038) converts a usable roof-area budget into
# a panel count via this. None for partial blocks lacking the fields.
panel_height_m: Optional[float] = None
panel_width_m: Optional[float] = None
@classmethod
def from_building_insights(
@ -111,18 +124,46 @@ class SolarPotential:
or "panelCapacityWatts" not in solar_potential
):
return None
# Per-segment centre + area live on the top-level `roofSegmentStats`,
# keyed by `segmentIndex`; the per-config `roofSegmentSummaries` carry
# only the panel/orientation fields. Build the lookup once.
stats_by_index: dict[int, Mapping[str, Any]] = {
int(stats["segmentIndex"]): stats
for stats in solar_potential.get("roofSegmentStats", [])
}
def _segment(summary: Mapping[str, Any]) -> SolarRoofSegment:
index: int = int(summary["segmentIndex"])
stats: Optional[Mapping[str, Any]] = stats_by_index.get(index)
center: Mapping[str, Any] = (
stats.get("center", {}) if stats is not None else {}
)
area: Optional[float] = (
float(stats["stats"]["areaMeters2"])
if stats is not None and "stats" in stats
else None
)
return SolarRoofSegment(
segment_index=index,
panels_count=int(summary["panelsCount"]),
azimuth_degrees=float(summary["azimuthDegrees"]),
pitch_degrees=float(summary["pitchDegrees"]),
yearly_energy_dc_kwh=float(summary["yearlyEnergyDcKwh"]),
center_latitude=(
float(center["latitude"]) if "latitude" in center else None
),
center_longitude=(
float(center["longitude"]) if "longitude" in center else None
),
area_m2=area,
)
configurations: tuple[SolarPanelConfiguration, ...] = tuple(
SolarPanelConfiguration(
panels_count=int(config["panelsCount"]),
yearly_energy_dc_kwh=float(config["yearlyEnergyDcKwh"]),
segments=tuple(
SolarRoofSegment(
segment_index=int(summary["segmentIndex"]),
panels_count=int(summary["panelsCount"]),
azimuth_degrees=float(summary["azimuthDegrees"]),
pitch_degrees=float(summary["pitchDegrees"]),
yearly_energy_dc_kwh=float(summary["yearlyEnergyDcKwh"]),
)
_segment(summary)
for summary in config.get("roofSegmentSummaries", [])
),
)
@ -132,4 +173,14 @@ class SolarPotential:
panel_capacity_watts=float(solar_potential["panelCapacityWatts"]),
max_array_panels_count=int(solar_potential["maxArrayPanelsCount"]),
configurations=configurations,
panel_height_m=(
float(solar_potential["panelHeightMeters"])
if "panelHeightMeters" in solar_potential
else None
),
panel_width_m=(
float(solar_potential["panelWidthMeters"])
if "panelWidthMeters" in solar_potential
else None
),
)