mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice 5a: the promotion. Replaces StubRebaseliner in production and collapses the
shadow runner into the rebaseliner (ADR-0013 amendment).
- CalculatorRebaseliner runs Sap10Calculator on every Property:
* sap_version < 10.2 -> Effective Performance IS the calculator output
(band via Epc.from_sap_score, CO2 kg->t, PEUI rounded), reason "pre_sap10".
* sap_version >= 10.2 -> Effective = lodged (API figures on-target), and the
calculator only logs divergence (SAP>0.5, PEUI/CO2 1%) as a validation signal.
* a calculator raise propagates -> batch aborts (ADR-0012); fix the cert at once.
- Rebaseliner.rebaseline gains property_id (for the divergence log).
- LoggingCalculatorShadow / the calculator_shadow seam removed from the
orchestrator; its divergence-comparison logic now lives in the rebaseliner.
- StubRebaseliner kept (signature updated) for orchestrator/repo unit tests.
The SapResult->EnergyBreakdown adapter + BillDerivation wiring (to populate the
bill block) follow once the appliances/cooking SapResult fields land.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
67 lines
2.7 KiB
Python
67 lines
2.7 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
|
|
from datatypes.epc.domain.epc_property_data import (
|
|
EpcPropertyData,
|
|
RenewableHeatIncentive,
|
|
)
|
|
from domain.property_baseline.property_baseline_performance import PropertyBaselinePerformance
|
|
from domain.property_baseline.performance import lodged_performance
|
|
from domain.property_baseline.rebaseliner import Rebaseliner
|
|
from repositories.unit_of_work import UnitOfWork
|
|
|
|
|
|
class PropertyBaselineOrchestrator:
|
|
"""Stage 2: establish each Property's Baseline Performance and persist it.
|
|
|
|
Runs the whole batch in **one** Unit of Work and commits once (ADR-0012):
|
|
for each property it hydrates the Property via the unit's PropertyRepo,
|
|
resolves the Effective EPC, reads Lodged Performance off it, runs the
|
|
Rebaseliner to produce Effective Performance, and persists the pair plus the
|
|
deterministic kWh. Any property raising aborts the batch — the unit is left
|
|
uncommitted, so nothing persists and the subtask fails noisily.
|
|
|
|
Reads only from repos — never a Fetcher or HTTP (ADR-0003) — so it is
|
|
byte-identical whether Ingestion ran milliseconds ago (First Run) or last
|
|
week. The injected Rebaseliner is the re-score-on-override seam (ADR-0011).
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
unit_of_work: Callable[[], UnitOfWork],
|
|
rebaseliner: Rebaseliner,
|
|
) -> None:
|
|
self._unit_of_work = unit_of_work
|
|
self._rebaseliner = rebaseliner
|
|
|
|
def run(self, property_ids: list[int]) -> None:
|
|
with self._unit_of_work() as uow:
|
|
properties = uow.property.get_many(property_ids)
|
|
for property_id, prop in zip(property_ids, properties, strict=True):
|
|
effective_epc = prop.effective_epc
|
|
lodged = lodged_performance(effective_epc)
|
|
effective, reason = self._rebaseliner.rebaseline(
|
|
property_id, effective_epc, lodged
|
|
)
|
|
rhi = _require_rhi(effective_epc)
|
|
baseline = PropertyBaselinePerformance(
|
|
lodged=lodged,
|
|
effective=effective,
|
|
rebaseline_reason=reason,
|
|
space_heating_kwh=rhi.space_heating_kwh,
|
|
water_heating_kwh=rhi.water_heating_kwh,
|
|
)
|
|
uow.property_baseline.save(baseline, property_id)
|
|
uow.commit()
|
|
|
|
|
|
def _require_rhi(epc: EpcPropertyData) -> RenewableHeatIncentive:
|
|
rhi = epc.renewable_heat_incentive
|
|
if rhi is None:
|
|
raise ValueError(
|
|
"Effective EPC is missing renewable_heat_incentive; cannot read "
|
|
"baseline space-heating / hot-water kWh"
|
|
)
|
|
return rhi
|