expose: buy/sell_price_now follow the live dual-tariff slot

Subscribe the separate DSMR tariff topic (dsmr/meter-stats/electricity_tariff,
1=dal/off-peak, 2=normal/peak), keep the latest slot in memory, and make the
manual-contract buy/sell_price_now sensors show the matching tariff's price
(tariff 1 -> dal, 2/unknown -> normal). Tibber (single 15-min price) unchanged.
Billing is untouched (register-split already handles tariffs correctly).

- new config dsmr_tariff_topic; subscribe managed by apply_dsmr_subscription
  (restart-free on config save). In-memory tariff is lock-guarded.
- import_cost_total / export_revenue_total cumulative getters unchanged.
This commit is contained in:
2026-06-24 15:39:21 +02:00
parent 6f8cb05eab
commit 9bc46f8d2d
9 changed files with 578 additions and 12 deletions
+208
View File
@@ -1117,3 +1117,211 @@ def test_export_revenue_total_falls_back_to_sum_when_no_active_contract(energy_d
assert value == pytest.approx(2.00, rel=1e-9), (
f"Expected pure SUM=2.00 with no active contract, got {value!r}"
)
# ---------------------------------------------------------------------------
# 15. Tariff-aware pricing: buy_price_now / sell_price_now honour _current_tariff
# ---------------------------------------------------------------------------
_DUAL_TARIFF_PRICING = {
"kind": "manual",
"buy_dal": "0.2500",
"buy_normal": "0.2700",
"sell_dal": "0.0900",
"sell_normal": "0.0950",
"energy_tax": "0.1234",
"ode": "0.0015",
}
@pytest.fixture()
def reset_tariff(monkeypatch):
"""Reset dsmr_ingest._current_tariff to None before/after each tariff test."""
from app.services import dsmr_ingest as _di
monkeypatch.setattr(_di, "_current_tariff", None)
yield
def _insert_manual_period(energy_db) -> None:
"""Insert a single non-degraded manual pricing period into energy_db."""
t0 = datetime(2026, 3, 1, 10, 0, tzinfo=timezone.utc)
with Session(energy_db) as session:
_make_period(
session,
period_start=t0,
import_cost=0.10,
export_revenue=0.05,
currency="EUR",
pricing=_DUAL_TARIFF_PRICING,
degraded=False,
)
session.commit()
def test_buy_price_tariff_1_returns_dal(energy_db, reset_tariff) -> None:
"""buy_price_now must return buy_dal when tariff=1 (off-peak)."""
from app.integrations.expose import build_catalog
from app.services.dsmr_ingest import set_current_tariff
_insert_manual_period(energy_db)
set_current_tariff(1)
with Session(energy_db) as session:
catalog = build_catalog(session)
buy_entry = next(e for e in catalog if e.entity.key == "energy.buy_price_now")
value = buy_entry.entity.value_getter(session)
assert value == pytest.approx(0.2500), f"Expected buy_dal=0.2500, got {value!r}"
def test_buy_price_tariff_2_returns_normal(energy_db, reset_tariff) -> None:
"""buy_price_now must return buy_normal when tariff=2 (peak)."""
from app.integrations.expose import build_catalog
from app.services.dsmr_ingest import set_current_tariff
_insert_manual_period(energy_db)
set_current_tariff(2)
with Session(energy_db) as session:
catalog = build_catalog(session)
buy_entry = next(e for e in catalog if e.entity.key == "energy.buy_price_now")
value = buy_entry.entity.value_getter(session)
assert value == pytest.approx(0.2700), f"Expected buy_normal=0.2700, got {value!r}"
def test_buy_price_tariff_none_falls_back_to_normal(energy_db, reset_tariff) -> None:
"""buy_price_now must return buy_normal when tariff is None (no DSMR tariff received yet)."""
from app.integrations.expose import build_catalog
from app.services.dsmr_ingest import set_current_tariff
_insert_manual_period(energy_db)
set_current_tariff(None) # explicitly None (no tariff received)
with Session(energy_db) as session:
catalog = build_catalog(session)
buy_entry = next(e for e in catalog if e.entity.key == "energy.buy_price_now")
value = buy_entry.entity.value_getter(session)
assert value == pytest.approx(0.2700), f"Expected buy_normal=0.2700 fallback, got {value!r}"
def test_sell_price_tariff_1_returns_dal(energy_db, reset_tariff) -> None:
"""sell_price_now must return sell_dal when tariff=1 (off-peak)."""
from app.integrations.expose import build_catalog
from app.services.dsmr_ingest import set_current_tariff
_insert_manual_period(energy_db)
set_current_tariff(1)
with Session(energy_db) as session:
catalog = build_catalog(session)
sell_entry = next(e for e in catalog if e.entity.key == "energy.sell_price_now")
value = sell_entry.entity.value_getter(session)
assert value == pytest.approx(0.0900), f"Expected sell_dal=0.0900, got {value!r}"
def test_sell_price_tariff_2_returns_normal(energy_db, reset_tariff) -> None:
"""sell_price_now must return sell_normal when tariff=2 (peak)."""
from app.integrations.expose import build_catalog
from app.services.dsmr_ingest import set_current_tariff
_insert_manual_period(energy_db)
set_current_tariff(2)
with Session(energy_db) as session:
catalog = build_catalog(session)
sell_entry = next(e for e in catalog if e.entity.key == "energy.sell_price_now")
value = sell_entry.entity.value_getter(session)
assert value == pytest.approx(0.0950), f"Expected sell_normal=0.0950, got {value!r}"
def test_sell_price_tariff_none_falls_back_to_normal(energy_db, reset_tariff) -> None:
"""sell_price_now must return sell_normal when tariff is None."""
from app.integrations.expose import build_catalog
from app.services.dsmr_ingest import set_current_tariff
_insert_manual_period(energy_db)
set_current_tariff(None)
with Session(energy_db) as session:
catalog = build_catalog(session)
sell_entry = next(e for e in catalog if e.entity.key == "energy.sell_price_now")
value = sell_entry.entity.value_getter(session)
assert value == pytest.approx(0.0950), f"Expected sell_normal=0.0950 fallback, got {value!r}"
def test_tibber_buy_price_not_affected_by_tariff(energy_db, reset_tariff) -> None:
"""Tibber buy_price_now must NOT be affected by the DSMR tariff — always uses 'buy' key."""
from app.integrations.expose import build_catalog
from app.services.dsmr_ingest import set_current_tariff
t0 = datetime(2026, 4, 1, 9, 0, tzinfo=timezone.utc)
tibber_pricing = {
"kind": "tibber",
"buy": "0.3100",
"sell": "0.1500",
"energy_tax": "0.1234",
"sell_adjust": "0.0100",
"total": "0.3100",
}
with Session(energy_db) as session:
_make_period(
session,
period_start=t0,
pricing=tibber_pricing,
degraded=False,
)
session.commit()
# Tibber pricing must return the same value regardless of tariff.
for tariff_val in (1, 2, None):
set_current_tariff(tariff_val)
with Session(energy_db) as session:
catalog = build_catalog(session)
buy_entry = next(e for e in catalog if e.entity.key == "energy.buy_price_now")
value = buy_entry.entity.value_getter(session)
assert value == pytest.approx(0.3100), (
f"Tibber buy price must be 0.3100 regardless of tariff={tariff_val!r}, got {value!r}"
)
def test_tibber_sell_price_not_affected_by_tariff(energy_db, reset_tariff) -> None:
"""Tibber sell_price_now must NOT be affected by the DSMR tariff — always uses 'sell' key."""
from app.integrations.expose import build_catalog
from app.services.dsmr_ingest import set_current_tariff
t0 = datetime(2026, 4, 1, 9, 15, tzinfo=timezone.utc)
tibber_pricing = {
"kind": "tibber",
"buy": "0.3100",
"sell": "0.1500",
"energy_tax": "0.1234",
"sell_adjust": "0.0100",
"total": "0.3100",
}
with Session(energy_db) as session:
_make_period(
session,
period_start=t0,
pricing=tibber_pricing,
degraded=False,
)
session.commit()
for tariff_val in (1, 2, None):
set_current_tariff(tariff_val)
with Session(energy_db) as session:
catalog = build_catalog(session)
sell_entry = next(e for e in catalog if e.entity.key == "energy.sell_price_now")
value = sell_entry.entity.value_getter(session)
assert value == pytest.approx(0.1500), (
f"Tibber sell price must be 0.1500 regardless of tariff={tariff_val!r}, got {value!r}"
)