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:
@@ -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}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user