mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice 102d: primary circuit loss via SAP 10.2 Table 3 with PCDB vessel gate
SAP 10.2 §4 line 7700 + Table 3 (PDF p.159) define the primary circuit
loss for cylinders heated indirectly through primary pipework:
(59)m = n_m × 14 × [{0.0091 × p + 0.0245 × (1 − p)} × h + 0.0263]
Inputs:
p pipework insulation fraction — Table 3 rows: 0.0 uninsulated,
0.1 first 1 m, 0.3 all accessible, 1.0 fully insulated. RdSAP §3
default table (PDF p.56) supplies p by construction age band:
bands A-J → 0.0, K, L, M → 1.0.
h hours per day of primary circulation, winter / summer split:
• no cylinder thermostat → 11 / 3
• thermostat, NOT separately timed → 5 / 3
• thermostat, separately timed → 3 / 3
("Use summer value for June, July, August and September and
winter value for other months" — spec p.159 footer.)
Spec p.159 lists the zero-loss configurations:
- electric immersion heater
- combi boiler
- CPSU
- thermal store within single casing
- separate boiler + thermal store within 1.5 m insulated pipe
- direct-acting electric boiler
- heat pump from PCDB with HW vessel integral to package
The cohort gate is now PCDB-aware: HP main + PCDB Table 362 record
`hw_vessel_mode != 1` (i.e. non-integral) → primary loss applies. All
7 cohort ASHPs lodge `hw_vessel_mode = 2` (separate and specified)
per Table 362 records 104568 (Mitsubishi) and 102421 (Daikin).
Cert 0380 (band D → p=0.0; cylinder thermostat + separately-timed →
h=3 / 3) lands (59)Jan = 31 × 14 × (0.0245 × 3 + 0.0263) = 43.3132
kWh/month (test pinned at 1e-4 vs cert's dr87 worksheet).
Cumulative cert 0380 API state:
HW kWh/yr 431.4 → 653.1 (target 878, slice 102e closes via η_water)
SAP 92.3 → 91.2 (delta to worksheet 88.51 now +2.73, was +3.75)
Cohort regression: cert 0390-2954 (oil boiler + cylinder, age F →
band A-J p=0.0) now picks up ~516 kWh/yr primary loss, tightening PE
residual -27.50 → -26.01 and CO2 -2.66 → -2.52 (improvements). The
higher HW fuel shifts SAP residual -6 → -7. Re-pinned with slice-102d
note. Closed combi boiler certs (001479, 0330, 9501) unaffected:
has_hot_water_cylinder=false gates the primary-loss override to None.
This commit is contained in:
parent
5b78a1e2c8
commit
c4a1045c8f
4 changed files with 264 additions and 8 deletions
|
|
@ -68,8 +68,14 @@ from domain.sap10_ml.sap_efficiencies import (
|
|||
water_heating_efficiency as _legacy_water_heating_efficiency,
|
||||
)
|
||||
from domain.sap10_calculator.calculator import CalculatorInputs
|
||||
from domain.sap10_calculator.tables.pcdb import gas_oil_boiler_record
|
||||
from domain.sap10_calculator.tables.pcdb.parser import GasOilBoilerRecord
|
||||
from domain.sap10_calculator.tables.pcdb import (
|
||||
gas_oil_boiler_record,
|
||||
heat_pump_record,
|
||||
)
|
||||
from domain.sap10_calculator.tables.pcdb.parser import (
|
||||
GasOilBoilerRecord,
|
||||
HeatPumpRecord,
|
||||
)
|
||||
from domain.sap10_calculator.tables.pcdb.postcode_weather import (
|
||||
PostcodeClimate,
|
||||
postcode_climate,
|
||||
|
|
@ -143,11 +149,14 @@ from domain.sap10_calculator.worksheet.ventilation import (
|
|||
ventilation_from_inputs,
|
||||
)
|
||||
from domain.sap10_calculator.worksheet.water_heating import (
|
||||
PIPEWORK_INSULATED_FULLY,
|
||||
PIPEWORK_INSULATED_UNINSULATED,
|
||||
TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
WaterHeatingResult,
|
||||
combi_loss_monthly_kwh_table_3b_row_1_instantaneous,
|
||||
combi_loss_monthly_kwh_table_3c_two_profile_instantaneous,
|
||||
cylinder_storage_loss_monthly_kwh,
|
||||
primary_loss_monthly_kwh,
|
||||
water_efficiency_monthly_via_equation_d1,
|
||||
water_heating_from_cert,
|
||||
)
|
||||
|
|
@ -1882,6 +1891,62 @@ def _separately_timed_dhw(main: Optional[MainHeatingDetail]) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
# RdSAP §3 default table (PDF p.56) — "Insulation of primary pipework":
|
||||
# age bands A-J → none (p=0.0); age bands K, L, M → full (p=1.0). The
|
||||
# default applies when the cert does not lodge an explicit insulation
|
||||
# fraction — which is the modal case for the Open EPC API (no field).
|
||||
_PIPEWORK_FULL_INSULATION_AGE_BANDS: Final[frozenset[str]] = frozenset(
|
||||
{"K", "L", "M"}
|
||||
)
|
||||
|
||||
|
||||
def _pipework_insulation_fraction_table_3(primary_age: Optional[str]) -> float:
|
||||
"""RdSAP §3 default for primary pipework insulation by age band.
|
||||
Bands K, L, M (post-2007) → 1.0 fully insulated; A-J → 0.0
|
||||
uninsulated. Unknown age band defaults to 0.0 (the conservative
|
||||
older-stock assumption matching cert 0380's worksheet 'Uninsulated
|
||||
primary pipework' lodgement).
|
||||
"""
|
||||
if primary_age in _PIPEWORK_FULL_INSULATION_AGE_BANDS:
|
||||
return PIPEWORK_INSULATED_FULLY
|
||||
return PIPEWORK_INSULATED_UNINSULATED
|
||||
|
||||
|
||||
def _primary_loss_applies(
|
||||
main: Optional[MainHeatingDetail],
|
||||
cylinder_present: bool,
|
||||
hp_record: Optional[HeatPumpRecord],
|
||||
) -> bool:
|
||||
"""SAP 10.2 Table 3 (PDF p.159) zero-loss configurations — primary
|
||||
loss only fires when a cylinder is present AND the lodgement falls
|
||||
outside the zero list. The cohort path: heat-pump main heating with
|
||||
a separate (not integral) vessel per the PCDB Table 362 record.
|
||||
|
||||
Combi boilers, CPSUs, thermal stores within 1.5 m insulated pipe,
|
||||
direct-acting electric boilers, electric immersion heaters, and
|
||||
HPs with `hw_vessel_mode = 1` (integral) all skip the loss. For
|
||||
cohort coverage we model two paths:
|
||||
- HP with PCDB record: gate on `hp_record.hw_vessel_mode != 1`
|
||||
- Boiler (cat 1, 2) with cylinder: primary loss applies (the
|
||||
cascade's pre-slice-102d behaviour was zero, masking ~516
|
||||
kWh/yr on certs with cylinders).
|
||||
"""
|
||||
if not cylinder_present:
|
||||
return False
|
||||
if main is None:
|
||||
return False
|
||||
if main.main_heating_category == 4:
|
||||
if hp_record is None:
|
||||
# No PCDB record → assume separate-vessel (conservative; the
|
||||
# zero-loss "integral vessel" branch requires explicit PCDB
|
||||
# confirmation per spec).
|
||||
return True
|
||||
# Spec p.159: zero for "Heat pump from PCDB with hot water vessel
|
||||
# integral to package". Vessel mode 1 = integral.
|
||||
return hp_record.hw_vessel_mode != 1
|
||||
return main.main_heating_category in {1, 2}
|
||||
|
||||
|
||||
def _table_3a_combi_loss_default_applies(main: Optional[MainHeatingDetail]) -> bool:
|
||||
"""Gate for the Table 3a keep-hot 600 kWh/yr fall-through per SAP 10.2
|
||||
§4 line 7702. Returns True only when the main heating system is in the
|
||||
|
|
@ -1942,6 +2007,10 @@ def _water_heating_worksheet_and_gains(
|
|||
# combi systems, so the override is only built when the cert explicitly
|
||||
# lodges a cylinder.
|
||||
storage_loss_override = _cylinder_storage_loss_override(epc, main)
|
||||
# SAP 10.2 §4 line 7700 + Table 3 (PDF p.159) — primary circuit loss
|
||||
# (59)m. Only fires for indirect cylinders; HPs with integral
|
||||
# vessels and combi boilers are in the spec's zero list.
|
||||
primary_loss_override = _primary_loss_override(epc, main, primary_age)
|
||||
wh_result = water_heating_from_cert(
|
||||
epc=epc,
|
||||
mixer_shower_flow_rates_l_per_min=_mixer_shower_flow_rates_from_cert(epc),
|
||||
|
|
@ -1950,12 +2019,41 @@ def _water_heating_worksheet_and_gains(
|
|||
low_water_use=False,
|
||||
combi_loss_monthly_kwh_override=combi_loss_override,
|
||||
solar_storage_monthly_kwh_override=storage_loss_override,
|
||||
primary_loss_monthly_kwh_override=primary_loss_override,
|
||||
has_electric_shower=has_electric_shower,
|
||||
electric_shower_count=electric_shower_count,
|
||||
)
|
||||
return wh_result, wh_result.heat_gains_monthly_kwh
|
||||
|
||||
|
||||
def _primary_loss_override(
|
||||
epc: EpcPropertyData,
|
||||
main: Optional[MainHeatingDetail],
|
||||
primary_age: Optional[str],
|
||||
) -> Optional[tuple[float, ...]]:
|
||||
"""Resolve (59)m for `water_heating_from_cert` from the cert + PCDB
|
||||
Table 362 record (for HP mains). Returns None when primary loss does
|
||||
not apply (combi boiler, integral-vessel HP, no cylinder, etc.) so
|
||||
the cascade keeps its zero default. Pipework insulation fraction p
|
||||
comes from RdSAP §3 age-band default (no API field); circulation
|
||||
hours h come from Table 3 keyed on cylinder thermostat + separately-
|
||||
timed-DHW lodgement.
|
||||
"""
|
||||
cylinder_present = bool(epc.has_hot_water_cylinder)
|
||||
hp_record: Optional[HeatPumpRecord] = None
|
||||
if main is not None and main.main_heating_index_number is not None:
|
||||
hp_record = heat_pump_record(main.main_heating_index_number)
|
||||
if not _primary_loss_applies(main, cylinder_present, hp_record):
|
||||
return None
|
||||
return primary_loss_monthly_kwh(
|
||||
pipework_insulation_fraction=_pipework_insulation_fraction_table_3(
|
||||
primary_age
|
||||
),
|
||||
has_cylinder_thermostat=epc.sap_heating.cylinder_thermostat == "Y",
|
||||
separately_timed_dhw=_separately_timed_dhw(main),
|
||||
)
|
||||
|
||||
|
||||
def _cylinder_storage_loss_override(
|
||||
epc: EpcPropertyData,
|
||||
main: Optional[MainHeatingDetail],
|
||||
|
|
|
|||
|
|
@ -1086,6 +1086,82 @@ def test_cert_with_hot_water_cylinder_computes_storage_loss_56m_from_sap_tables_
|
|||
)
|
||||
|
||||
|
||||
def test_cert_with_hot_water_cylinder_computes_primary_loss_59m_from_sap_table_3() -> None:
|
||||
"""SAP 10.2 §4 line 7700 + Table 3 (PDF p.159) define the primary
|
||||
circuit loss for an indirect cylinder:
|
||||
(59)m = n_m × 14 × [{0.0091 × p + 0.0245 × (1 − p)} × h + 0.0263]
|
||||
where
|
||||
n_m = days in month,
|
||||
p = fraction of primary pipework insulated (0.0 uninsulated,
|
||||
0.1 first 1m, 0.3 all accessible, 1.0 fully insulated),
|
||||
h = hours per day of circulation (5 winter / 3 summer if
|
||||
cylinder thermostat present; 3 / 3 if DHW separately
|
||||
timed; 11 / 3 if no cylinder thermostat).
|
||||
|
||||
RdSAP §3 default table (PDF p.56) supplies pipework insulation by
|
||||
age band: bands A-J → none (p=0.0); bands K, L, M → full (p=1.0).
|
||||
|
||||
Cert 0380 (band D = 1950-1966 → p=0.0; cylinder thermostat lodged
|
||||
+ separately-timed DHW → h=3 winter and summer) yields
|
||||
(59)Jan = 31 × 14 × (0.0245 × 3 + 0.0263)
|
||||
= 31 × 14 × 0.0998
|
||||
= 43.3132 kWh/month
|
||||
matching the cert 0380 dr87 worksheet pin to 4 d.p.
|
||||
|
||||
Spec PDF p.159 lists configurations for which the primary loss is
|
||||
zero ("Combi boiler", "Electric immersion heater", "Heat pump from
|
||||
PCDB with hot water vessel integral to package", etc.). Cert 0380
|
||||
uses a heat pump with separate-and-specified vessel
|
||||
(`hw_vessel_mode = 2` in PCDB Table 362), so the loss applies.
|
||||
"""
|
||||
# Arrange — synthetic ASHP cert mirroring cert 0380: cat=4, PCDB
|
||||
# 104568 (Mitsubishi 5 kW Ecodan, separate-specified vessel),
|
||||
# cylinder lodged with thermostat, separately-timed DHW, age band D.
|
||||
hp_main = MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
main_fuel_type=29,
|
||||
heat_emitter_type=1,
|
||||
emitter_temperature=1,
|
||||
main_heating_control=2206,
|
||||
main_heating_category=4,
|
||||
sap_main_heating_code=None,
|
||||
main_heating_index_number=104568,
|
||||
)
|
||||
epc = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=_TYPICAL_TFA_M2,
|
||||
habitable_rooms_count=4,
|
||||
country_code="ENG",
|
||||
has_hot_water_cylinder=True,
|
||||
sap_building_parts=[make_building_part(construction_age_band="D")],
|
||||
sap_heating=make_sap_heating(
|
||||
main_heating_details=[hp_main],
|
||||
water_heating_code=901,
|
||||
cylinder_size=3,
|
||||
cylinder_insulation_type=1,
|
||||
cylinder_insulation_thickness_mm=50,
|
||||
cylinder_thermostat="Y",
|
||||
),
|
||||
)
|
||||
|
||||
# Act
|
||||
wh_result, _ = _water_heating_worksheet_and_gains(
|
||||
epc=epc,
|
||||
water_efficiency_pct=1.7,
|
||||
is_instantaneous=False,
|
||||
primary_age="D",
|
||||
pcdb_record=None,
|
||||
)
|
||||
|
||||
# Assert — (59)m Jan matches worksheet at 1e-4.
|
||||
assert wh_result is not None
|
||||
expected_jan_kwh = 43.3132
|
||||
got_jan_kwh = wh_result.primary_loss_monthly_kwh[0]
|
||||
assert abs(got_jan_kwh - expected_jan_kwh) < 1e-4, (
|
||||
f"(59)Jan: got {got_jan_kwh!r}, want {expected_jan_kwh!r} per "
|
||||
f"SAP 10.2 §4 line 7700 + Table 3"
|
||||
)
|
||||
|
||||
|
||||
def test_air_source_heat_pump_main_heating_zeroes_table_3a_combi_loss_per_sap_4_line_7702() -> None:
|
||||
"""SAP 10.2 §4 line 7702 worksheet defines (61)m as 'Combi loss for
|
||||
each month from Table 3a, 3b or 3c (enter "0" if not a combi
|
||||
|
|
|
|||
|
|
@ -120,16 +120,20 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
_GoldenExpectation(
|
||||
cert_number="0390-2954-3640-2196-4175",
|
||||
actual_sap=60,
|
||||
expected_sap_resid=-6,
|
||||
expected_pe_resid_kwh_per_m2=-27.5026,
|
||||
expected_co2_resid_tonnes_per_yr=-2.6570,
|
||||
expected_sap_resid=-7,
|
||||
expected_pe_resid_kwh_per_m2=-26.0093,
|
||||
expected_co2_resid_tonnes_per_yr=-2.5211,
|
||||
notes=(
|
||||
"Large detached, TFA 360, age F, oil PCDB-listed. Cert lodges "
|
||||
"has_draught_lobby=true and a 160 L factory-insulated cylinder. "
|
||||
"Slice 97 added glazing_type=2 — windows now drop to spec U=2.0, "
|
||||
"widening PE → -28.68 and CO2 → -2.76. Slice 102b then applied "
|
||||
"SAP 10.2 Tables 2/2a/2b cylinder storage loss (~432 kWh/yr), "
|
||||
"tightening PE -28.68 → -27.50 and CO2 -2.76 → -2.66."
|
||||
"tightening PE -28.68 → -27.50 and CO2 -2.76 → -2.66. Slice 102d "
|
||||
"then added SAP 10.2 Table 3 primary circuit loss (~516 kWh/yr "
|
||||
"uninsulated, age band F → A-J default p=0.0), tightening PE "
|
||||
"-27.50 → -26.01, CO2 -2.66 → -2.52, and shifting SAP residual "
|
||||
"-6 → -7 (cost of the higher HW fuel)."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ class WaterHeatingResult:
|
|||
energy_content_monthly_kwh: tuple[float, ...]
|
||||
distribution_loss_monthly_kwh: tuple[float, ...]
|
||||
solar_storage_monthly_kwh: tuple[float, ...] # (57)m — Tables 2/2a/2b
|
||||
primary_loss_monthly_kwh: tuple[float, ...] # (59)m — Table 3
|
||||
combi_loss_monthly_kwh: tuple[float, ...]
|
||||
total_demand_monthly_kwh: tuple[float, ...]
|
||||
output_monthly_kwh: tuple[float, ...]
|
||||
|
|
@ -487,6 +488,76 @@ def cylinder_temperature_factor_table_2b(
|
|||
return factor
|
||||
|
||||
|
||||
# SAP 10.2 Table 3 (PDF p.159) — primary circuit loss for boilers and
|
||||
# heat pumps connected to a hot water cylinder via insulated or
|
||||
# uninsulated primary pipework. The spec lists the zero-loss
|
||||
# configurations explicitly (combi boilers, integral-vessel heat pumps,
|
||||
# CPSUs, thermal stores within 1.5 m insulated pipe, etc.); callers
|
||||
# must gate this helper on those exemptions.
|
||||
PIPEWORK_INSULATED_UNINSULATED: Final[float] = 0.0
|
||||
PIPEWORK_INSULATED_FIRST_METRE: Final[float] = 0.1
|
||||
PIPEWORK_INSULATED_ALL_ACCESSIBLE: Final[float] = 0.3
|
||||
PIPEWORK_INSULATED_FULLY: Final[float] = 1.0
|
||||
|
||||
# Per Table 3 hours-per-day table: 5 winter / 3 summer if cylinder
|
||||
# thermostat present and water heating not separately timed; 3 / 3 if
|
||||
# cylinder thermostat present AND separately timed; 11 / 3 if no
|
||||
# cylinder thermostat. "Use summer value for June, July, August and
|
||||
# September and winter value for other months."
|
||||
_SUMMER_MONTH_INDICES: Final[tuple[int, ...]] = (5, 6, 7, 8) # Jun..Sep
|
||||
|
||||
|
||||
def primary_circuit_hours_per_day_table_3(
|
||||
*,
|
||||
has_cylinder_thermostat: bool,
|
||||
separately_timed_dhw: bool,
|
||||
) -> tuple[float, float]:
|
||||
"""SAP 10.2 Table 3 (PDF p.159) — hours of primary circulation per
|
||||
day, returned as `(winter_hours, summer_hours)`:
|
||||
no thermostat → (11, 3)
|
||||
thermostat, not separately timed → ( 5, 3)
|
||||
thermostat, separately timed → ( 3, 3)
|
||||
"""
|
||||
if not has_cylinder_thermostat:
|
||||
return (11.0, 3.0)
|
||||
if separately_timed_dhw:
|
||||
return (3.0, 3.0)
|
||||
return (5.0, 3.0)
|
||||
|
||||
|
||||
def primary_loss_monthly_kwh(
|
||||
*,
|
||||
pipework_insulation_fraction: float,
|
||||
has_cylinder_thermostat: bool,
|
||||
separately_timed_dhw: bool,
|
||||
) -> tuple[float, ...]:
|
||||
"""SAP 10.2 §4 line (59)m via Table 3 (PDF p.159):
|
||||
(59)m = n_m × 14 × [{0.0091 × p + 0.0245 × (1 − p)} × h + 0.0263]
|
||||
where p is the fraction of primary pipework insulated and h is the
|
||||
hours of primary circulation per day (winter / summer split per
|
||||
`primary_circuit_hours_per_day_table_3`).
|
||||
|
||||
Returns 12 monthly values in calendar order Jan..Dec. Callers must
|
||||
gate this helper on the spec's zero-loss configurations
|
||||
(combi boilers, integral-vessel HPs, CPSUs, thermal stores ≤ 1.5 m
|
||||
insulated pipe, etc.) — the formula assumes the configuration
|
||||
incurs the loss.
|
||||
"""
|
||||
p = pipework_insulation_fraction
|
||||
pipework_term = 0.0091 * p + 0.0245 * (1.0 - p)
|
||||
winter_h, summer_h = primary_circuit_hours_per_day_table_3(
|
||||
has_cylinder_thermostat=has_cylinder_thermostat,
|
||||
separately_timed_dhw=separately_timed_dhw,
|
||||
)
|
||||
return tuple(
|
||||
n * 14.0 * (
|
||||
pipework_term * (summer_h if m in _SUMMER_MONTH_INDICES else winter_h)
|
||||
+ 0.0263
|
||||
)
|
||||
for m, n in enumerate(_DAYS_IN_MONTH)
|
||||
)
|
||||
|
||||
|
||||
def cylinder_storage_loss_monthly_kwh(
|
||||
*,
|
||||
volume_l: float,
|
||||
|
|
@ -728,6 +799,7 @@ def water_heating_from_cert(
|
|||
low_water_use: bool,
|
||||
combi_loss_monthly_kwh_override: Optional[tuple[float, ...]] = None,
|
||||
solar_storage_monthly_kwh_override: Optional[tuple[float, ...]] = None,
|
||||
primary_loss_monthly_kwh_override: Optional[tuple[float, ...]] = None,
|
||||
electric_shower_monthly_kwh_override: Optional[tuple[float, ...]] = None,
|
||||
has_electric_shower: bool = False,
|
||||
electric_shower_count: int = 0,
|
||||
|
|
@ -813,11 +885,16 @@ def water_heating_from_cert(
|
|||
if solar_storage_monthly_kwh_override is not None
|
||||
else zero12
|
||||
)
|
||||
primary_loss = (
|
||||
primary_loss_monthly_kwh_override
|
||||
if primary_loss_monthly_kwh_override is not None
|
||||
else zero12
|
||||
)
|
||||
total_demand = total_water_heating_demand_monthly_kwh(
|
||||
energy_content_monthly_kwh=energy_content,
|
||||
distribution_loss_monthly_kwh=distribution,
|
||||
solar_storage_monthly_kwh=solar_storage,
|
||||
primary_loss_monthly_kwh=zero12,
|
||||
primary_loss_monthly_kwh=primary_loss,
|
||||
combi_loss_monthly_kwh=combi,
|
||||
)
|
||||
output = output_from_water_heater_monthly_kwh(
|
||||
|
|
@ -845,7 +922,7 @@ def water_heating_from_cert(
|
|||
energy_content_monthly_kwh=energy_content,
|
||||
distribution_loss_monthly_kwh=distribution,
|
||||
solar_storage_monthly_kwh=solar_storage,
|
||||
primary_loss_monthly_kwh=zero12,
|
||||
primary_loss_monthly_kwh=primary_loss,
|
||||
combi_loss_monthly_kwh=combi,
|
||||
electric_shower_monthly_kwh=electric_shower,
|
||||
)
|
||||
|
|
@ -856,6 +933,7 @@ def water_heating_from_cert(
|
|||
energy_content_monthly_kwh=energy_content,
|
||||
distribution_loss_monthly_kwh=distribution,
|
||||
solar_storage_monthly_kwh=solar_storage,
|
||||
primary_loss_monthly_kwh=primary_loss,
|
||||
combi_loss_monthly_kwh=combi,
|
||||
total_demand_monthly_kwh=total_demand,
|
||||
output_monthly_kwh=output,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue