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
+92
View File
@@ -410,3 +410,95 @@ def test_timestamp_with_utc_offset_suffix(dsmr_db):
_call_handle_message(data, settings, SessionLocal)
assert _count_readings(SessionLocal) == 1
# ---------------------------------------------------------------------------
# 10. handle_tariff_message: parse, validate, update, reject invalid payloads
# ---------------------------------------------------------------------------
@pytest.fixture()
def reset_tariff(monkeypatch):
"""Reset _current_tariff to None before and after each tariff test."""
from app.services import dsmr_ingest as _di
monkeypatch.setattr(_di, "_current_tariff", None)
yield
# monkeypatch auto-restores on teardown
def test_tariff_message_value_2_sets_tariff(reset_tariff):
"""Payload b'2' must set the current tariff to 2 (normal/peak)."""
from app.services.dsmr_ingest import handle_tariff_message, get_current_tariff
handle_tariff_message(b"2")
assert get_current_tariff() == 2
def test_tariff_message_value_1_sets_tariff(reset_tariff):
"""Payload b'1' must set the current tariff to 1 (dal/off-peak)."""
from app.services.dsmr_ingest import handle_tariff_message, get_current_tariff
handle_tariff_message(b"1")
assert get_current_tariff() == 1
def test_tariff_message_updates_from_2_to_1(reset_tariff):
"""Subsequent payloads must overwrite the previous tariff value."""
from app.services.dsmr_ingest import handle_tariff_message, get_current_tariff
handle_tariff_message(b"2")
assert get_current_tariff() == 2
handle_tariff_message(b"1")
assert get_current_tariff() == 1
def test_tariff_message_strips_whitespace(reset_tariff):
"""Payloads with surrounding whitespace (e.g. b'2\\n') must be accepted."""
from app.services.dsmr_ingest import handle_tariff_message, get_current_tariff
handle_tariff_message(b"2\n")
assert get_current_tariff() == 2
handle_tariff_message(b" 1 ")
assert get_current_tariff() == 1
def test_tariff_message_invalid_non_numeric_does_not_update(reset_tariff):
"""Non-numeric payload must not update the tariff; previous value is preserved."""
from app.services.dsmr_ingest import handle_tariff_message, get_current_tariff, set_current_tariff
set_current_tariff(2)
handle_tariff_message(b"x")
# Must NOT raise and must NOT change the tariff.
assert get_current_tariff() == 2
def test_tariff_message_invalid_empty_does_not_update(reset_tariff):
"""Empty payload must not update the tariff; previous value is preserved."""
from app.services.dsmr_ingest import handle_tariff_message, get_current_tariff, set_current_tariff
set_current_tariff(1)
handle_tariff_message(b"")
assert get_current_tariff() == 1
def test_tariff_message_out_of_range_value_does_not_update(reset_tariff):
"""Payload with out-of-range integer (not 1 or 2) must not update the tariff."""
from app.services.dsmr_ingest import handle_tariff_message, get_current_tariff, set_current_tariff
set_current_tariff(2)
handle_tariff_message(b"3") # 3 is not a valid tariff
assert get_current_tariff() == 2
handle_tariff_message(b"0") # 0 is not a valid tariff
assert get_current_tariff() == 2
def test_tariff_message_does_not_raise_on_any_input(reset_tariff):
"""handle_tariff_message must never propagate any exception to the caller."""
from app.services.dsmr_ingest import handle_tariff_message
# All of these must complete without raising.
for payload in (b"", b"x", b"99", b"\xff\xfe", b"None", b"2.0"):
handle_tariff_message(payload) # must not raise