M8-T05: bind electricity costs to source bindings

This commit is contained in:
2026-08-23 21:22:05 +02:00
parent 2e125dbd53
commit 1ea2f659e0
10 changed files with 1057 additions and 304 deletions
+12 -11
View File
@@ -430,9 +430,7 @@ def test_prices_tibber_limit_caps_results(energy_client):
start = (datetime.now(UTC) - timedelta(hours=3)).isoformat()
end = (datetime.now(UTC) + timedelta(hours=2)).isoformat()
resp = client.get(
"/api/energy/prices", params={"start": start, "end": end, "limit": 2}
)
resp = client.get("/api/energy/prices", params={"start": start, "end": end, "limit": 2})
assert resp.status_code == 200
body = resp.json()
assert len(body["points"]) <= 2
@@ -508,9 +506,16 @@ def test_costs_schema_fields_present(energy_client):
item = resp.json()["items"][0]
for field in (
"period_start",
"d1_kwh", "d2_kwh", "r1_kwh", "r2_kwh",
"import_cost", "export_revenue", "net_cost",
"currency", "degraded",
"d1_kwh",
"d2_kwh",
"r1_kwh",
"r2_kwh",
"import_cost",
"export_revenue",
"net_cost",
"currency",
"degraded",
"source_binding_id",
):
assert field in item, f"Missing field: {field}"
@@ -556,9 +561,7 @@ def test_summary_returns_correct_structure(energy_client):
start = (datetime.now(UTC) - timedelta(hours=3)).isoformat()
end = datetime.now(UTC).isoformat()
resp = client.get(
"/api/energy/costs/summary", params={"start": start, "end": end}
)
resp = client.get("/api/energy/costs/summary", params={"start": start, "end": end})
assert resp.status_code == 200
body = resp.json()
for field in (
@@ -903,5 +906,3 @@ def test_tibber_test_token_not_in_response(energy_client):
assert resp.status_code == 200
assert secret_token not in resp.text
+855 -243
View File
File diff suppressed because it is too large Load Diff
+35 -5
View File
@@ -1097,6 +1097,9 @@ def test_import_cost_total_includes_standing_charges(energy_db) -> None:
from app.services import timezone as _tz_mod
now_utc = datetime.now(timezone.utc)
# Keep the dynamic test date, but settle it at a deterministic business time
# beyond the local 01:05 fixed-fee/credit cutoff.
settled_local_now = now_utc.replace(hour=12, minute=0, second=0, microsecond=0)
# D2 anchor = meter.started_at = 10 UTC days ago at midnight
meter_started_at = now_utc.replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(days=10)
effective_from = meter_started_at # contract also starts at the same time
@@ -1123,7 +1126,10 @@ def test_import_cost_total_includes_standing_charges(energy_db) -> None:
with Session(energy_db) as session:
# Pin to UTC so local days = UTC days (deterministic on any CI host).
with patch.object(_tz_mod, "local_tz", return_value=ZoneInfo("UTC")):
with (
patch.object(_tz_mod, "local_tz", return_value=ZoneInfo("UTC")),
patch("app.services.energy_cost.local_now", return_value=settled_local_now),
):
catalog = build_catalog(session)
import_entry = next(
e for e in catalog if e.entity.key == "energy.import_cost_total"
@@ -1163,6 +1169,9 @@ def test_export_revenue_total_includes_tax_credit(energy_db) -> None:
from app.services import timezone as _tz_mod
now_utc = datetime.now(timezone.utc)
# Keep the dynamic test date, but settle it at a deterministic business time
# beyond the local 01:05 fixed-fee/credit cutoff.
settled_local_now = now_utc.replace(hour=12, minute=0, second=0, microsecond=0)
meter_started_at = now_utc.replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(days=4)
effective_from = meter_started_at
@@ -1186,7 +1195,10 @@ def test_export_revenue_total_includes_tax_credit(energy_db) -> None:
session.commit()
with Session(energy_db) as session:
with patch.object(_tz_mod, "local_tz", return_value=ZoneInfo("UTC")):
with (
patch.object(_tz_mod, "local_tz", return_value=ZoneInfo("UTC")),
patch("app.services.energy_cost.local_now", return_value=settled_local_now),
):
catalog = build_catalog(session)
export_entry = next(
e for e in catalog if e.entity.key == "energy.export_revenue_total"
@@ -1945,6 +1957,9 @@ def test_cumulative_anchor_is_meter_started_at(energy_db) -> None:
from app.services import timezone as _tz_mod
now_utc = datetime.now(timezone.utc)
# Keep the dynamic test date, but settle it at a deterministic business time
# beyond the local 01:05 fixed-fee/credit cutoff.
settled_local_now = now_utc.replace(hour=12, minute=0, second=0, microsecond=0)
midnight_today = now_utc.replace(hour=0, minute=0, second=0, microsecond=0)
# Contract starts 180 days ago (far before the meter)
@@ -1966,7 +1981,10 @@ def test_cumulative_anchor_is_meter_started_at(energy_db) -> None:
session.commit()
with Session(energy_db) as session:
with patch.object(_tz_mod, "local_tz", return_value=ZoneInfo("UTC")):
with (
patch.object(_tz_mod, "local_tz", return_value=ZoneInfo("UTC")),
patch("app.services.energy_cost.local_now", return_value=settled_local_now),
):
catalog = build_catalog(session)
import_entry = next(
e for e in catalog if e.entity.key == "energy.import_cost_total"
@@ -2089,6 +2107,9 @@ def test_cumulative_resets_after_meter_swap(energy_db) -> None:
from app.services import timezone as _tz_mod
now_utc = datetime.now(timezone.utc)
# Keep the dynamic test date, but settle it at a deterministic business time
# beyond the local 01:05 fixed-fee/credit cutoff.
settled_local_now = now_utc.replace(hour=12, minute=0, second=0, microsecond=0)
midnight_today = now_utc.replace(hour=0, minute=0, second=0, microsecond=0)
old_meter_start = midnight_today - timedelta(days=30)
@@ -2145,7 +2166,10 @@ def test_cumulative_resets_after_meter_swap(energy_db) -> None:
session.commit()
with Session(energy_db) as session:
with patch.object(_tz_mod, "local_tz", return_value=ZoneInfo("UTC")):
with (
patch.object(_tz_mod, "local_tz", return_value=ZoneInfo("UTC")),
patch("app.services.energy_cost.local_now", return_value=settled_local_now),
):
catalog = build_catalog(session)
import_entry = next(
e for e in catalog if e.entity.key == "energy.import_cost_total"
@@ -2194,6 +2218,9 @@ def test_daily_getters_unaffected_by_d2_meter_anchor(energy_db) -> None:
from app.services import timezone as _tz_mod
now_utc = datetime.now(timezone.utc)
# Keep the dynamic test date, but settle it at a deterministic business time
# beyond the local 01:05 fixed-fee/credit cutoff.
settled_local_now = now_utc.replace(hour=12, minute=0, second=0, microsecond=0)
# Period 1h ago — in today's UTC window
t0 = now_utc.replace(minute=0, second=0, microsecond=0) - timedelta(hours=1)
if t0.date() < now_utc.date():
@@ -2226,7 +2253,10 @@ def test_daily_getters_unaffected_by_d2_meter_anchor(energy_db) -> None:
session.commit()
with Session(energy_db) as session:
with patch.object(_tz_mod, "local_tz", return_value=ZoneInfo("UTC")):
with (
patch.object(_tz_mod, "local_tz", return_value=ZoneInfo("UTC")),
patch("app.services.energy_cost.local_now", return_value=settled_local_now),
):
catalog = build_catalog(session)
import_today_entry = next(
e for e in catalog if e.entity.key == "energy.import_cost_today"