"""The glazing Recommendation Generator (double / secondary glazing). Detects a dwelling's single-glazed windows and emits one "Windows" Recommendation carrying a single, planning-picked Measure Option (ADR-0022). Unlike the wall generator's competing EWI/IWI Options, the Property's `PlanningRestrictions` *hard-picks* the Measure: an unrestricted dwelling gets `double_glazing`; any conservation/listed/heritage protection (i.e. `blocks_external`) forces `secondary_glazing` — an internal second pane that leaves the protected external units untouched. All single-glazed windows are upgraded together in one overlay. The overlay writes each window's lodged `u_value` and `solar_transmittance` (not just `glazing_type`), because our calculator reads those per-window values directly from `WindowTransmissionDetails` rather than deriving them from the glazing type (`heat_transmission.py:490`, `solar_gains.py:300`); `glazing_type` is set too, for the §5 daylight factor. The target values are pinned from cert 001431's before→after re-lodgement. Detection + pricing only; impact is produced later by scoring (ADR-0016). """ from dataclasses import dataclass from typing import Final, Optional from datatypes.epc.domain.epc_property_data import EpcPropertyData from domain.geospatial.planning_restrictions import PlanningRestrictions from domain.modelling.measure_type import MeasureType from domain.modelling.recommendation import Cost, MeasureOption, Recommendation from domain.modelling.simulation import EpcSimulation, WindowOverlay from repositories.product.product_repository import ProductRepository # Single-glazing codes — the only windows this generator upgrades. Code 1 is # bare "Single"/"Single glazing"; code 15 is "single glazing, known data" (a # single pane with manufacturer U/g lodged — same g_L=0.90 as code 1, per # RdSAP-21). Both are single-glazed and must be detected, or a cert that lodges # manufacturer data on its single panes (e.g. 001431 windows 12-13) is missed. _SINGLE_GLAZED_CODES: Final[frozenset[int]] = frozenset({1, 15}) @dataclass(frozen=True) class _GlazingTarget: """The planning-picked Measure and the per-window values its overlay lodges, pinned from cert 001431's before→after (`glazing_type`, `u_value`, `solar_transmittance` — SAP10.2 Table U2 code, then heat-loss U and solar g). """ measure_type: MeasureType description: str glazing_type: int u_value: float solar_transmittance: float # Unrestricted: replace the units with double glazing (gt=5 "Double post 2022"; # U 4.80→1.40, g 0.85→0.72). _DOUBLE: Final[_GlazingTarget] = _GlazingTarget( measure_type=MeasureType.DOUBLE_GLAZING, description="Replace the single-glazed windows with double glazing", glazing_type=5, u_value=1.40, solar_transmittance=0.72, ) # Protected (conservation/listed/heritage): fit an internal secondary pane # (gt=11 "Secondary glazing - Normal emissivity", what cert 001431 re-lodges; # U→2.90, g unchanged at 0.85 — the existing outer single pane still drives # solar gain). The external units can't be replaced on a protected/over-looked # building, so this is the planning-picked Measure. _SECONDARY: Final[_GlazingTarget] = _GlazingTarget( measure_type=MeasureType.SECONDARY_GLAZING, description="Fit secondary glazing to the single-glazed windows", glazing_type=11, u_value=2.90, solar_transmittance=0.85, ) def recommend_glazing( epc: EpcPropertyData, products: ProductRepository, restrictions: PlanningRestrictions = PlanningRestrictions(), ) -> Optional[Recommendation]: """Return a glazing Recommendation upgrading every single-glazed window — its single planning-picked Option (double glazing, or secondary glazing where a planning protection blocks replacing the external units) — else None when the dwelling has no single-glazed windows.""" single_indices = tuple( index for index, window in enumerate(epc.sap_windows) if window.glazing_type in _SINGLE_GLAZED_CODES ) if not single_indices: return None target: _GlazingTarget = _SECONDARY if restrictions.blocks_external else _DOUBLE product = products.get(target.measure_type) overlay = EpcSimulation( windows={ index: WindowOverlay( glazing_type=target.glazing_type, u_value=target.u_value, solar_transmittance=target.solar_transmittance, ) for index in single_indices } ) cost = Cost( total=len(single_indices) * product.unit_cost_per_m2, contingency_rate=product.contingency_rate, ) option = MeasureOption( measure_type=target.measure_type, description=target.description, overlay=overlay, cost=cost, material_id=product.id, ) return Recommendation(surface="Windows", options=(option,))