expose: fold standing fee into import cost, tax credit into export revenue

So HA can show the full payable: net = import - export = Total Payable, with
both entities staying monotonic-positive (no negative-value/reset pitfalls).

- import_cost_total = SUM(import_cost) + days*(network_fee+management_fee)/30
- export_revenue_total = SUM(export_revenue) + days*heffingskorting/365
  (days = calendar days since the active version's effective_from, incl. today)
- state_class total_increasing -> total (monetary-compatible; values still only
  increase). device_class/key/names: key unchanged, names clarified.
- buy/sell_price_now and DeviceInfo untouched. Decimal money math. Tests added.
This commit is contained in:
2026-06-24 14:26:32 +02:00
parent effe434637
commit 6f8cb05eab
2 changed files with 387 additions and 19 deletions
+65 -10
View File
@@ -511,12 +511,23 @@ def _energy_cost_provider(session: Session) -> list[ExposableEntity]:
return _getter
# --- value_getter: cumulative import cost ---
# --- value_getter: cumulative import cost (incl. standing charges) ---
def _make_import_cost_getter() -> Callable[["Session"], Any]:
"""Return a getter for the cumulative import cost (non-degraded rows only)."""
"""Return a getter for the cumulative import cost plus prorated standing charges.
Value = SUM(import_cost, non-degraded) + standing_charges_cumulative
where standing_charges_cumulative = elapsed_days × (network_fee + management_fee) / 30.
Elapsed days are counted from the active contract version's effective_from date
(inclusive of today). Returns None when no non-degraded periods exist.
"""
def _getter(sess: "Session") -> Any:
from decimal import Decimal
from datetime import UTC, datetime as _dt
from app.models.energy import EnergyCostPeriod as _ECP
from app.services.contracts import active_contract_version_at, _as_utc
from sqlalchemy import func as _func
total = sess.query(_func.sum(_ECP.import_cost)).filter(
@@ -524,16 +535,44 @@ def _energy_cost_provider(session: Session) -> list[ExposableEntity]:
).scalar()
if total is None:
return None
return float(total)
# Add prorated standing charges from the active contract version.
standing_cumulative = Decimal("0")
now_utc = _dt.now(UTC)
version = active_contract_version_at(sess, now_utc)
if version is not None:
vals = version.values or {}
standing = vals.get("standing", {})
network_fee = Decimal(str(standing.get("network_fee") or 0))
management_fee = Decimal(str(standing.get("management_fee") or 0))
daily_standing = (network_fee + management_fee) / Decimal("30")
effective_from = _as_utc(version.effective_from)
days = (now_utc.date() - effective_from.date()).days + 1
days = max(days, 0)
standing_cumulative = daily_standing * days
return float(Decimal(str(total)) + standing_cumulative)
return _getter
# --- value_getter: cumulative export revenue ---
# --- value_getter: cumulative export revenue (incl. tax credit) ---
def _make_export_revenue_getter() -> Callable[["Session"], Any]:
"""Return a getter for the cumulative export revenue (non-degraded rows only)."""
"""Return a getter for the cumulative export revenue plus prorated tax credit.
Value = SUM(export_revenue, non-degraded) + heffingskorting_cumulative
where heffingskorting_cumulative = elapsed_days × heffingskorting / 365.
Elapsed days are counted from the active contract version's effective_from date
(inclusive of today). Returns None when no non-degraded periods exist.
"""
def _getter(sess: "Session") -> Any:
from decimal import Decimal
from datetime import UTC, datetime as _dt
from app.models.energy import EnergyCostPeriod as _ECP
from app.services.contracts import active_contract_version_at, _as_utc
from sqlalchemy import func as _func
total = sess.query(_func.sum(_ECP.export_revenue)).filter(
@@ -541,7 +580,23 @@ def _energy_cost_provider(session: Session) -> list[ExposableEntity]:
).scalar()
if total is None:
return None
return float(total)
# Add prorated heffingskorting credit from the active contract version.
credit_cumulative = Decimal("0")
now_utc = _dt.now(UTC)
version = active_contract_version_at(sess, now_utc)
if version is not None:
vals = version.values or {}
credits = vals.get("credits", {})
heffingskorting = Decimal(str(credits.get("heffingskorting") or 0))
daily_credit = heffingskorting / Decimal("365")
effective_from = _as_utc(version.effective_from)
days = (now_utc.date() - effective_from.date()).days + 1
days = max(days, 0)
credit_cumulative = daily_credit * days
return float(Decimal(str(total)) + credit_cumulative)
return _getter
@@ -575,9 +630,9 @@ def _energy_cost_provider(session: Session) -> list[ExposableEntity]:
device=device_info,
device_class="monetary",
unit=currency,
name="Energy Import Cost Total",
name="Energy Import Cost (incl. standing)",
value_getter=_make_import_cost_getter(),
state_class="total_increasing",
state_class="total",
),
ExposableEntity(
key="energy.export_revenue_total",
@@ -585,9 +640,9 @@ def _energy_cost_provider(session: Session) -> list[ExposableEntity]:
device=device_info,
device_class="monetary",
unit=currency,
name="Energy Export Revenue Total",
name="Energy Export Revenue (incl. tax credit)",
value_getter=_make_export_revenue_getter(),
state_class="total_increasing",
state_class="total",
),
]