M8-R17: allocate thermal standing costs by service

This commit is contained in:
2026-08-28 14:24:57 +02:00
parent 6e197d7808
commit 5e1545efad
4 changed files with 90 additions and 4 deletions
+54 -1
View File
@@ -3002,6 +3002,11 @@ def test_m8_thermal_today_summary_ends_at_frozen_now(energy_db) -> None:
"heating": Decimal("0"), "hot_water_heating": Decimal("1.25"),
"hot_water": Decimal("2.75"), "hot_water_tax": Decimal("9.99"),
},
"fixed_breakdown": {
"heating_network": Decimal("0"), "metering": Decimal("0"),
"delivery_set": Decimal("0"), "hot_water_network": Decimal("0"),
"other": Decimal("0"),
},
}
def summarize_spy(_session: Session, start: datetime, end: datetime, *, now: datetime) -> dict:
@@ -3023,5 +3028,53 @@ def test_m8_thermal_today_summary_ends_at_frozen_now(energy_db) -> None:
assert entity.value_getter(session) == Decimal("0")
hot_water_total = next(item.entity for item in build_catalog(session)
if item.entity.key.endswith(".hot_water_total_today"))
assert hot_water_total.value_getter(session) == Decimal("4.00")
assert hot_water_total.value_getter(session) == Decimal("13.99")
assert captured["end"] == now
@pytest.mark.parametrize("suffix", ("total", "today"))
def test_m8_thermal_totals_allocate_standing_costs_and_match_all_in(energy_db, suffix: str) -> None:
"""Heating and hot water each receive their assigned costs, without overlap."""
from app.integrations.expose import build_catalog
from app.services import timezone as tz
now = datetime(2026, 1, 15, 10, tzinfo=timezone.utc)
summary = {
"period_count": 2,
"fixed_cost": Decimal("35"),
"total_cost": Decimal("54"),
"breakdown": {
"heating": Decimal("10"), "hot_water_heating": Decimal("2"),
"hot_water": Decimal("3"), "hot_water_tax": Decimal("4"),
},
"fixed_breakdown": {
"heating_network": Decimal("5"), "metering": Decimal("6"),
"delivery_set": Decimal("7"), "hot_water_network": Decimal("9"),
"other": Decimal("8"),
},
}
with Session(energy_db) as session:
_make_thermal_source_and_meter(session, "heating", now)
_make_thermal_source_and_meter(session, "hot_water", now)
session.commit()
with (
patch("app.integrations.expose._utc_now", return_value=now),
patch.object(tz, "local_tz", return_value=ZoneInfo("Europe/Amsterdam")),
patch.object(tz, "local_now", return_value=now.astimezone(ZoneInfo("Europe/Amsterdam"))),
patch("app.services.meter_cost.summarize", return_value=summary),
):
entities = {item.entity.key.rsplit(".", 1)[-1]: item.entity
for item in build_catalog(session) if item.entity.key.startswith("thermal_cost.")}
heating = entities[f"heating_{suffix}"].value_getter(session)
hot_water = entities[f"hot_water_total_{suffix}"].value_getter(session)
assert heating == Decimal("36") # 10 + heating_network + metering + delivery_set + other
assert hot_water == Decimal("18") # 2 + 3 + 4 + hot_water_network
assert entities[f"fixed_{suffix}"].value_getter(session) == Decimal("35")
assert entities[f"all_in_{suffix}"].value_getter(session) == Decimal("54")
assert heating + hot_water == entities[f"all_in_{suffix}"].value_getter(session)
# Component entities deliberately remain variable-only.
assert entities[f"hot_water_heating_{suffix}"].value_getter(session) == Decimal("2")
assert entities[f"water_{suffix}"].value_getter(session) == Decimal("3")
assert entities[f"water_tax_{suffix}"].value_getter(session) == Decimal("4")