mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
Load the off-peak day/night rate from the committed snapshot 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bda8a3f276
commit
1acfc08fce
2 changed files with 32 additions and 8 deletions
|
|
@ -5,7 +5,7 @@ from pathlib import Path
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from domain.fuel_rates.fuel import Fuel
|
from domain.fuel_rates.fuel import Fuel
|
||||||
from domain.fuel_rates.fuel_rates import FuelRate, FuelRates
|
from domain.fuel_rates.fuel_rates import FuelRate, FuelRates, OffPeakRate
|
||||||
from repositories.fuel_rates.fuel_rates_repository import FuelRatesRepository
|
from repositories.fuel_rates.fuel_rates_repository import FuelRatesRepository
|
||||||
|
|
||||||
_DEFAULT_SNAPSHOT = Path(__file__).parent / "data" / "fuel_rates_2026_q2.json"
|
_DEFAULT_SNAPSHOT = Path(__file__).parent / "data" / "fuel_rates_2026_q2.json"
|
||||||
|
|
@ -14,10 +14,12 @@ _DEFAULT_SNAPSHOT = Path(__file__).parent / "data" / "fuel_rates_2026_q2.json"
|
||||||
class FuelRatesStaticFileRepository(FuelRatesRepository):
|
class FuelRatesStaticFileRepository(FuelRatesRepository):
|
||||||
"""Reads Fuel Rates from a committed JSON snapshot (ADR-0014).
|
"""Reads Fuel Rates from a committed JSON snapshot (ADR-0014).
|
||||||
|
|
||||||
Only **single-rate** fuels (those lodging a ``unit_rate_p_per_kwh``) are
|
**Single-rate** fuels (those lodging a ``unit_rate_p_per_kwh``) populate the
|
||||||
exposed. Off-peak (day/night) and the unpriced gaps (null entries — house
|
``rates`` map. The **off-peak** entry — a dual-rate meter lodging
|
||||||
coal, heat network) are skipped, so pricing them raises ``UnpricedFuel``.
|
``day_p_per_kwh`` / ``night_p_per_kwh`` — loads as an ``OffPeakRate`` (each
|
||||||
The day/night accessor for off-peak lands in a later slice.
|
end use blends day/night by its own High-Rate Fraction). The unpriced gaps
|
||||||
|
(null entries — house coal, heat network) are skipped, so pricing them
|
||||||
|
raises ``UnpricedFuel``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, snapshot_path: Optional[Path] = None) -> None:
|
def __init__(self, snapshot_path: Optional[Path] = None) -> None:
|
||||||
|
|
@ -27,11 +29,19 @@ class FuelRatesStaticFileRepository(FuelRatesRepository):
|
||||||
payload: dict[str, Any] = json.loads(self._snapshot_path.read_text())
|
payload: dict[str, Any] = json.loads(self._snapshot_path.read_text())
|
||||||
fuels: dict[str, Any] = payload["fuels"]
|
fuels: dict[str, Any] = payload["fuels"]
|
||||||
rates: dict[Fuel, FuelRate] = {}
|
rates: dict[Fuel, FuelRate] = {}
|
||||||
|
off_peak: Optional[OffPeakRate] = None
|
||||||
for name, entry in fuels.items():
|
for name, entry in fuels.items():
|
||||||
if entry is None:
|
if entry is None:
|
||||||
continue # an unpriced gap (house coal / heat network)
|
continue # an unpriced gap (house coal / heat network)
|
||||||
|
if name == Fuel.ELECTRICITY_OFF_PEAK.name:
|
||||||
|
off_peak = OffPeakRate(
|
||||||
|
day_p_per_kwh=float(entry["day_p_per_kwh"]),
|
||||||
|
night_p_per_kwh=float(entry["night_p_per_kwh"]),
|
||||||
|
standing_charge_p_per_day=float(entry["standing_charge_p_per_day"]),
|
||||||
|
)
|
||||||
|
continue
|
||||||
if "unit_rate_p_per_kwh" not in entry:
|
if "unit_rate_p_per_kwh" not in entry:
|
||||||
continue # off-peak day/night — priced in a later slice
|
continue # an unpriced gap with no single rate
|
||||||
rates[Fuel[name]] = FuelRate(
|
rates[Fuel[name]] = FuelRate(
|
||||||
unit_rate_p_per_kwh=float(entry["unit_rate_p_per_kwh"]),
|
unit_rate_p_per_kwh=float(entry["unit_rate_p_per_kwh"]),
|
||||||
standing_charge_p_per_day=float(entry["standing_charge_p_per_day"]),
|
standing_charge_p_per_day=float(entry["standing_charge_p_per_day"]),
|
||||||
|
|
@ -40,4 +50,5 @@ class FuelRatesStaticFileRepository(FuelRatesRepository):
|
||||||
period=str(payload["period"]),
|
period=str(payload["period"]),
|
||||||
seg_export_p_per_kwh=float(payload["seg_export_p_per_kwh"]),
|
seg_export_p_per_kwh=float(payload["seg_export_p_per_kwh"]),
|
||||||
rates=rates,
|
rates=rates,
|
||||||
|
off_peak=off_peak,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,21 @@ def test_dual_fuel_carries_a_derived_midpoint_rate() -> None:
|
||||||
assert rates.standing_charge_p_per_day(Fuel.DUAL_FUEL_MINERAL_AND_WOOD) == 0.0
|
assert rates.standing_charge_p_per_day(Fuel.DUAL_FUEL_MINERAL_AND_WOOD) == 0.0
|
||||||
|
|
||||||
|
|
||||||
def test_off_peak_remains_unpriced_pending_the_day_night_accessor() -> None:
|
def test_off_peak_meter_prices_day_night_from_the_snapshot() -> None:
|
||||||
# Arrange — off-peak still needs the day/night split a later slice adds (ADR-0014).
|
# Arrange — the committed snapshot's off-peak entry carries a day/night
|
||||||
|
# split (ADR-0014 2026-06-24 amendment); the repo loads it as an OffPeakRate.
|
||||||
|
rates = FuelRatesStaticFileRepository().get_current()
|
||||||
|
|
||||||
|
# Act / Assert — an all-night load bills at the night rate, an all-day load
|
||||||
|
# at the day rate, and the off-peak meter carries its own standing charge.
|
||||||
|
assert rates.off_peak_blended_p_per_kwh(high_rate_fraction=0.0) == 13.89
|
||||||
|
assert rates.off_peak_blended_p_per_kwh(high_rate_fraction=1.0) == 29.73
|
||||||
|
assert rates.standing_charge_p_per_day(Fuel.ELECTRICITY_OFF_PEAK) == 56.99
|
||||||
|
|
||||||
|
|
||||||
|
def test_off_peak_has_no_single_unit_rate() -> None:
|
||||||
|
# Arrange — off-peak has no single blended unit rate; callers must price it
|
||||||
|
# day/night via a High-Rate Fraction, so unit_rate_p_per_kwh still raises.
|
||||||
rates = FuelRatesStaticFileRepository().get_current()
|
rates = FuelRatesStaticFileRepository().get_current()
|
||||||
|
|
||||||
# Act / Assert
|
# Act / Assert
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue