mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
233 lines
8.8 KiB
Python
233 lines
8.8 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from domain.fuel_rates.fuel import Fuel
|
|
from domain.billing.bill import BillSection, EnergyBreakdown
|
|
from domain.sap10_calculator.calculator import SapResult
|
|
|
|
|
|
def _sap_result(
|
|
*,
|
|
main_heating_fuel_kwh_per_yr: float = 0.0,
|
|
main_heating_fuel_code: int | None = None,
|
|
main_2_heating_fuel_kwh_per_yr: float = 0.0,
|
|
main_2_heating_fuel_code: int | None = None,
|
|
secondary_heating_fuel_kwh_per_yr: float = 0.0,
|
|
secondary_heating_fuel_code: int | None = None,
|
|
hot_water_kwh_per_yr: float = 0.0,
|
|
hot_water_fuel_code: int | None = None,
|
|
space_cooling_fuel_kwh_per_yr: float = 0.0,
|
|
pumps_fans_kwh_per_yr: float = 0.0,
|
|
lighting_kwh_per_yr: float = 0.0,
|
|
appliances_kwh_per_yr: float = 0.0,
|
|
cooking_kwh_per_yr: float = 0.0,
|
|
pv_exported_kwh_per_yr: float = 0.0,
|
|
is_off_peak_meter: bool = False,
|
|
main_heating_high_rate_fraction: float = 1.0,
|
|
main_2_heating_high_rate_fraction: float = 1.0,
|
|
secondary_heating_high_rate_fraction: float = 1.0,
|
|
hot_water_high_rate_fraction: float = 1.0,
|
|
pumps_fans_high_rate_fraction: float = 1.0,
|
|
other_electricity_high_rate_fraction: float = 1.0,
|
|
) -> SapResult:
|
|
return SapResult(
|
|
sap_score=72,
|
|
sap_score_continuous=72.0,
|
|
ecf=0.0,
|
|
total_fuel_cost_gbp=0.0,
|
|
co2_kg_per_yr=0.0,
|
|
space_heating_kwh_per_yr=0.0,
|
|
space_cooling_kwh_per_yr=0.0,
|
|
fabric_energy_efficiency_kwh_per_m2_yr=0.0,
|
|
main_heating_fuel_kwh_per_yr=main_heating_fuel_kwh_per_yr,
|
|
main_2_heating_fuel_kwh_per_yr=main_2_heating_fuel_kwh_per_yr,
|
|
secondary_heating_fuel_kwh_per_yr=secondary_heating_fuel_kwh_per_yr,
|
|
space_cooling_fuel_kwh_per_yr=space_cooling_fuel_kwh_per_yr,
|
|
hot_water_kwh_per_yr=hot_water_kwh_per_yr,
|
|
pumps_fans_kwh_per_yr=pumps_fans_kwh_per_yr,
|
|
lighting_kwh_per_yr=lighting_kwh_per_yr,
|
|
appliances_kwh_per_yr=appliances_kwh_per_yr,
|
|
cooking_kwh_per_yr=cooking_kwh_per_yr,
|
|
main_heating_fuel_code=main_heating_fuel_code,
|
|
main_2_heating_fuel_code=main_2_heating_fuel_code,
|
|
secondary_heating_fuel_code=secondary_heating_fuel_code,
|
|
hot_water_fuel_code=hot_water_fuel_code,
|
|
pv_exported_kwh_per_yr=pv_exported_kwh_per_yr,
|
|
primary_energy_kwh_per_yr=0.0,
|
|
primary_energy_kwh_per_m2=0.0,
|
|
monthly=(),
|
|
intermediate={},
|
|
is_off_peak_meter=is_off_peak_meter,
|
|
main_heating_high_rate_fraction=main_heating_high_rate_fraction,
|
|
main_2_heating_high_rate_fraction=main_2_heating_high_rate_fraction,
|
|
secondary_heating_high_rate_fraction=secondary_heating_high_rate_fraction,
|
|
hot_water_high_rate_fraction=hot_water_high_rate_fraction,
|
|
pumps_fans_high_rate_fraction=pumps_fans_high_rate_fraction,
|
|
other_electricity_high_rate_fraction=other_electricity_high_rate_fraction,
|
|
)
|
|
|
|
|
|
def test_each_positive_end_use_becomes_a_line_at_its_fuel() -> None:
|
|
# Arrange — a gas-boiler home with an electric secondary heater: HEATING
|
|
# carries two lines on different fuels; HW is gas; the rest are electricity.
|
|
result = _sap_result(
|
|
main_heating_fuel_kwh_per_yr=8000.0,
|
|
main_heating_fuel_code=1, # mains gas
|
|
secondary_heating_fuel_kwh_per_yr=300.0,
|
|
secondary_heating_fuel_code=30, # electricity
|
|
hot_water_kwh_per_yr=2500.0,
|
|
hot_water_fuel_code=1, # mains gas
|
|
lighting_kwh_per_yr=400.0,
|
|
appliances_kwh_per_yr=1900.0,
|
|
cooking_kwh_per_yr=300.0,
|
|
)
|
|
|
|
# Act
|
|
breakdown = EnergyBreakdown.from_sap_result(result)
|
|
|
|
# Assert
|
|
lines = {(line.section, line.fuel): line.kwh for line in breakdown.lines}
|
|
assert lines == {
|
|
(BillSection.HEATING, Fuel.MAINS_GAS): 8000.0,
|
|
(BillSection.HEATING, Fuel.ELECTRICITY): 300.0,
|
|
(BillSection.HOT_WATER, Fuel.MAINS_GAS): 2500.0,
|
|
(BillSection.LIGHTING, Fuel.ELECTRICITY): 400.0,
|
|
(BillSection.APPLIANCES, Fuel.ELECTRICITY): 1900.0,
|
|
(BillSection.COOKING, Fuel.ELECTRICITY): 300.0,
|
|
}
|
|
|
|
|
|
def test_zero_kwh_end_uses_emit_no_line() -> None:
|
|
# Arrange — only lighting has energy; everything else is zero.
|
|
result = _sap_result(lighting_kwh_per_yr=350.0)
|
|
|
|
# Act
|
|
breakdown = EnergyBreakdown.from_sap_result(result)
|
|
|
|
# Assert — exactly one line, no empty HEATING / HOT_WATER / COOLING entries.
|
|
assert len(breakdown.lines) == 1
|
|
assert breakdown.lines[0].section == BillSection.LIGHTING
|
|
|
|
|
|
def test_cooling_is_billed_as_electricity() -> None:
|
|
# Arrange — a home with fixed cooling.
|
|
result = _sap_result(space_cooling_fuel_kwh_per_yr=450.0)
|
|
|
|
# Act
|
|
breakdown = EnergyBreakdown.from_sap_result(result)
|
|
|
|
# Assert
|
|
assert len(breakdown.lines) == 1
|
|
line = breakdown.lines[0]
|
|
assert (line.section, line.fuel, line.kwh) == (
|
|
BillSection.COOLING,
|
|
Fuel.ELECTRICITY,
|
|
450.0,
|
|
)
|
|
|
|
|
|
def test_pv_export_carries_to_exported_kwh() -> None:
|
|
# Arrange
|
|
result = _sap_result(lighting_kwh_per_yr=400.0, pv_exported_kwh_per_yr=1200.0)
|
|
|
|
# Act
|
|
breakdown = EnergyBreakdown.from_sap_result(result)
|
|
|
|
# Assert
|
|
assert breakdown.exported_kwh == 1200.0
|
|
|
|
|
|
def test_raw_api_fuel_code_is_normalized_to_its_billing_fuel() -> None:
|
|
# Arrange — the calculator can carry a raw gov-API fuel code (26 = mains gas).
|
|
result = _sap_result(
|
|
main_heating_fuel_kwh_per_yr=9000.0, main_heating_fuel_code=26
|
|
)
|
|
|
|
# Act
|
|
breakdown = EnergyBreakdown.from_sap_result(result)
|
|
|
|
# Assert
|
|
assert breakdown.lines[0].fuel == Fuel.MAINS_GAS
|
|
|
|
|
|
def test_positive_heating_kwh_with_no_fuel_code_raises() -> None:
|
|
# Arrange — energy with no resolvable fuel is a data gap, not a default.
|
|
result = _sap_result(
|
|
main_heating_fuel_kwh_per_yr=8000.0, main_heating_fuel_code=None
|
|
)
|
|
|
|
# Act / Assert
|
|
with pytest.raises(ValueError, match="no fuel code"):
|
|
EnergyBreakdown.from_sap_result(result)
|
|
|
|
|
|
def test_off_peak_heating_line_carries_its_carrier_and_high_rate_fraction() -> None:
|
|
# Arrange — electric storage heating on an Off-Peak Meter (7-hour low-rate
|
|
# code 31), charged wholly overnight (main heating high-rate fraction 0.0).
|
|
result = _sap_result(
|
|
main_heating_fuel_kwh_per_yr=9000.0,
|
|
main_heating_fuel_code=31,
|
|
is_off_peak_meter=True,
|
|
main_heating_high_rate_fraction=0.0,
|
|
)
|
|
|
|
# Act
|
|
breakdown = EnergyBreakdown.from_sap_result(result)
|
|
|
|
# Assert — the heating line bills on the off-peak carrier with the
|
|
# calculator's high-rate fraction attached for the day/night split.
|
|
line = breakdown.lines[0]
|
|
assert (line.section, line.fuel, line.kwh, line.high_rate_fraction) == (
|
|
BillSection.HEATING,
|
|
Fuel.ELECTRICITY_OFF_PEAK,
|
|
9000.0,
|
|
0.0,
|
|
)
|
|
|
|
|
|
def test_off_peak_meter_routes_every_electric_use_to_the_off_peak_carrier() -> None:
|
|
# Arrange — a gas-heated dwelling that happens to be on an Off-Peak Meter:
|
|
# the gas heating stays gas, but ALL electric uses (lighting, appliances,
|
|
# pumps) bill on the off-peak carrier, the "other" uses sharing the
|
|
# ALL_OTHER_USES fraction and pumps/fans carrying their own.
|
|
result = _sap_result(
|
|
main_heating_fuel_kwh_per_yr=8000.0,
|
|
main_heating_fuel_code=1, # mains gas — single-rate, unaffected
|
|
lighting_kwh_per_yr=400.0,
|
|
appliances_kwh_per_yr=1900.0,
|
|
pumps_fans_kwh_per_yr=200.0,
|
|
is_off_peak_meter=True,
|
|
other_electricity_high_rate_fraction=0.90,
|
|
pumps_fans_high_rate_fraction=0.71,
|
|
)
|
|
|
|
# Act
|
|
breakdown = EnergyBreakdown.from_sap_result(result)
|
|
|
|
# Assert
|
|
by_section = {line.section: line for line in breakdown.lines}
|
|
assert by_section[BillSection.HEATING].fuel is Fuel.MAINS_GAS
|
|
assert by_section[BillSection.HEATING].high_rate_fraction is None
|
|
assert by_section[BillSection.LIGHTING].fuel is Fuel.ELECTRICITY_OFF_PEAK
|
|
assert by_section[BillSection.LIGHTING].high_rate_fraction == 0.90
|
|
assert by_section[BillSection.APPLIANCES].fuel is Fuel.ELECTRICITY_OFF_PEAK
|
|
assert by_section[BillSection.APPLIANCES].high_rate_fraction == 0.90
|
|
assert by_section[BillSection.PUMPS_FANS].high_rate_fraction == 0.71
|
|
|
|
|
|
def test_standard_meter_keeps_electric_uses_on_flat_rate_electricity() -> None:
|
|
# Arrange — electric heating on a standard meter: no day/night split anywhere.
|
|
result = _sap_result(
|
|
main_heating_fuel_kwh_per_yr=6000.0,
|
|
main_heating_fuel_code=30, # standard-tariff electricity
|
|
lighting_kwh_per_yr=400.0,
|
|
is_off_peak_meter=False,
|
|
)
|
|
|
|
# Act
|
|
breakdown = EnergyBreakdown.from_sap_result(result)
|
|
|
|
# Assert — every line is flat-rate ELECTRICITY with no high-rate fraction.
|
|
assert all(line.fuel is Fuel.ELECTRICITY for line in breakdown.lines)
|
|
assert all(line.high_rate_fraction is None for line in breakdown.lines)
|