M8-R00: align tariff expose tests with source bindings
This commit is contained in:
+81
-37
@@ -38,8 +38,8 @@ from zoneinfo import ZoneInfo
|
|||||||
import pytest
|
import pytest
|
||||||
from alembic import command
|
from alembic import command
|
||||||
from alembic.config import Config
|
from alembic.config import Config
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine, select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session, sessionmaker
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1426,25 +1426,59 @@ _DUAL_TARIFF_PRICING = {
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def reset_tariff(monkeypatch):
|
def reset_tariff(energy_db, monkeypatch):
|
||||||
"""Reset dsmr_ingest._current_tariff to None before/after each tariff test."""
|
"""Isolate legacy and source-scoped DSMR tariff state for each tariff test."""
|
||||||
from app.services import dsmr_ingest as _di
|
from app.services import dsmr_ingest as _di
|
||||||
|
|
||||||
monkeypatch.setattr(_di, "_current_tariff", None)
|
test_session_local = sessionmaker(bind=energy_db)
|
||||||
yield
|
monkeypatch.setattr(_di, "get_session_local", lambda: test_session_local)
|
||||||
|
_di._current_tariff = None
|
||||||
|
with _di._tariff_lock:
|
||||||
|
_di._tariffs.clear()
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
_di._current_tariff = None
|
||||||
|
with _di._tariff_lock:
|
||||||
|
_di._tariffs.clear()
|
||||||
|
|
||||||
|
|
||||||
def _insert_manual_period(energy_db) -> None:
|
def _bind_migrated_dsmr_source(session: Session, meter: Any, *, now: datetime) -> int:
|
||||||
"""Insert an active electricity meter + non-degraded manual pricing period into energy_db.
|
"""Bind the migration-created DSMR source to an active electricity meter."""
|
||||||
|
from app.models.meter_source import MeterSource, MeterSourceBinding, MeterSourceChannel
|
||||||
|
|
||||||
|
source = session.scalar(select(MeterSource).where(MeterSource.kind == "dsmr_mqtt"))
|
||||||
|
assert source is not None, "Alembic head must provide the migrated DSMR source"
|
||||||
|
channel = session.scalar(
|
||||||
|
select(MeterSourceChannel).where(MeterSourceChannel.source_id == source.id)
|
||||||
|
)
|
||||||
|
assert channel is not None, "Alembic head must provide the migrated DSMR channel"
|
||||||
|
session.add(
|
||||||
|
MeterSourceBinding(
|
||||||
|
meter_id=meter.id,
|
||||||
|
channel_id=channel.id,
|
||||||
|
started_at=meter.started_at,
|
||||||
|
ended_at=None,
|
||||||
|
created_at=now,
|
||||||
|
updated_at=now,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
session.flush()
|
||||||
|
return source.id
|
||||||
|
|
||||||
|
|
||||||
|
def _insert_manual_period(energy_db) -> int:
|
||||||
|
"""Insert a bound active electricity meter and manual pricing period.
|
||||||
|
|
||||||
FUE-T05: the energy-cost provider requires an active electricity meter, so
|
FUE-T05: the energy-cost provider requires an active electricity meter, so
|
||||||
this helper inserts one alongside the period so that build_catalog returns
|
this helper binds its migration-created DSMR source to that meter. Return
|
||||||
the energy entities.
|
the binding's source id so tariff tests exercise source-scoped state.
|
||||||
"""
|
"""
|
||||||
t0 = datetime(2026, 3, 1, 10, 0, tzinfo=timezone.utc)
|
t0 = datetime(2026, 3, 1, 10, 0, tzinfo=timezone.utc)
|
||||||
meter_start = datetime(2026, 3, 1, 0, 0, tzinfo=timezone.utc)
|
meter_start = datetime(2026, 3, 1, 0, 0, tzinfo=timezone.utc)
|
||||||
with Session(energy_db) as session:
|
with Session(energy_db) as session:
|
||||||
_make_active_meter(session, started_at=meter_start, label="Tariff Test Meter")
|
meter = _make_active_meter(session, started_at=meter_start, label="Tariff Test Meter")
|
||||||
|
source_id = _bind_migrated_dsmr_source(session, meter, now=t0)
|
||||||
_make_period(
|
_make_period(
|
||||||
session,
|
session,
|
||||||
period_start=t0,
|
period_start=t0,
|
||||||
@@ -1455,6 +1489,7 @@ def _insert_manual_period(energy_db) -> None:
|
|||||||
degraded=False,
|
degraded=False,
|
||||||
)
|
)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
return source_id
|
||||||
|
|
||||||
|
|
||||||
def test_buy_price_tariff_1_returns_dal(energy_db, reset_tariff) -> None:
|
def test_buy_price_tariff_1_returns_dal(energy_db, reset_tariff) -> None:
|
||||||
@@ -1462,8 +1497,8 @@ def test_buy_price_tariff_1_returns_dal(energy_db, reset_tariff) -> None:
|
|||||||
from app.integrations.expose import build_catalog
|
from app.integrations.expose import build_catalog
|
||||||
from app.services.dsmr_ingest import set_current_tariff
|
from app.services.dsmr_ingest import set_current_tariff
|
||||||
|
|
||||||
_insert_manual_period(energy_db)
|
source_id = _insert_manual_period(energy_db)
|
||||||
set_current_tariff(1)
|
set_current_tariff(source_id, 1)
|
||||||
|
|
||||||
with Session(energy_db) as session:
|
with Session(energy_db) as session:
|
||||||
catalog = build_catalog(session)
|
catalog = build_catalog(session)
|
||||||
@@ -1478,8 +1513,8 @@ def test_buy_price_tariff_2_returns_normal(energy_db, reset_tariff) -> None:
|
|||||||
from app.integrations.expose import build_catalog
|
from app.integrations.expose import build_catalog
|
||||||
from app.services.dsmr_ingest import set_current_tariff
|
from app.services.dsmr_ingest import set_current_tariff
|
||||||
|
|
||||||
_insert_manual_period(energy_db)
|
source_id = _insert_manual_period(energy_db)
|
||||||
set_current_tariff(2)
|
set_current_tariff(source_id, 2)
|
||||||
|
|
||||||
with Session(energy_db) as session:
|
with Session(energy_db) as session:
|
||||||
catalog = build_catalog(session)
|
catalog = build_catalog(session)
|
||||||
@@ -1494,8 +1529,8 @@ def test_buy_price_tariff_none_falls_back_to_normal(energy_db, reset_tariff) ->
|
|||||||
from app.integrations.expose import build_catalog
|
from app.integrations.expose import build_catalog
|
||||||
from app.services.dsmr_ingest import set_current_tariff
|
from app.services.dsmr_ingest import set_current_tariff
|
||||||
|
|
||||||
_insert_manual_period(energy_db)
|
source_id = _insert_manual_period(energy_db)
|
||||||
set_current_tariff(None) # explicitly None (no tariff received)
|
set_current_tariff(source_id, None) # explicitly None (no tariff received)
|
||||||
|
|
||||||
with Session(energy_db) as session:
|
with Session(energy_db) as session:
|
||||||
catalog = build_catalog(session)
|
catalog = build_catalog(session)
|
||||||
@@ -1510,8 +1545,8 @@ def test_sell_price_tariff_1_returns_dal(energy_db, reset_tariff) -> None:
|
|||||||
from app.integrations.expose import build_catalog
|
from app.integrations.expose import build_catalog
|
||||||
from app.services.dsmr_ingest import set_current_tariff
|
from app.services.dsmr_ingest import set_current_tariff
|
||||||
|
|
||||||
_insert_manual_period(energy_db)
|
source_id = _insert_manual_period(energy_db)
|
||||||
set_current_tariff(1)
|
set_current_tariff(source_id, 1)
|
||||||
|
|
||||||
with Session(energy_db) as session:
|
with Session(energy_db) as session:
|
||||||
catalog = build_catalog(session)
|
catalog = build_catalog(session)
|
||||||
@@ -1526,8 +1561,8 @@ def test_sell_price_tariff_2_returns_normal(energy_db, reset_tariff) -> None:
|
|||||||
from app.integrations.expose import build_catalog
|
from app.integrations.expose import build_catalog
|
||||||
from app.services.dsmr_ingest import set_current_tariff
|
from app.services.dsmr_ingest import set_current_tariff
|
||||||
|
|
||||||
_insert_manual_period(energy_db)
|
source_id = _insert_manual_period(energy_db)
|
||||||
set_current_tariff(2)
|
set_current_tariff(source_id, 2)
|
||||||
|
|
||||||
with Session(energy_db) as session:
|
with Session(energy_db) as session:
|
||||||
catalog = build_catalog(session)
|
catalog = build_catalog(session)
|
||||||
@@ -1542,8 +1577,8 @@ def test_sell_price_tariff_none_falls_back_to_normal(energy_db, reset_tariff) ->
|
|||||||
from app.integrations.expose import build_catalog
|
from app.integrations.expose import build_catalog
|
||||||
from app.services.dsmr_ingest import set_current_tariff
|
from app.services.dsmr_ingest import set_current_tariff
|
||||||
|
|
||||||
_insert_manual_period(energy_db)
|
source_id = _insert_manual_period(energy_db)
|
||||||
set_current_tariff(None)
|
set_current_tariff(source_id, None)
|
||||||
|
|
||||||
with Session(energy_db) as session:
|
with Session(energy_db) as session:
|
||||||
catalog = build_catalog(session)
|
catalog = build_catalog(session)
|
||||||
@@ -1573,7 +1608,8 @@ def test_tibber_buy_price_not_affected_by_tariff(energy_db, reset_tariff) -> Non
|
|||||||
}
|
}
|
||||||
|
|
||||||
with Session(energy_db) as session:
|
with Session(energy_db) as session:
|
||||||
_make_active_meter(session, started_at=meter_start)
|
meter = _make_active_meter(session, started_at=meter_start)
|
||||||
|
source_id = _bind_migrated_dsmr_source(session, meter, now=t0)
|
||||||
_make_period(
|
_make_period(
|
||||||
session,
|
session,
|
||||||
period_start=t0,
|
period_start=t0,
|
||||||
@@ -1584,7 +1620,7 @@ def test_tibber_buy_price_not_affected_by_tariff(energy_db, reset_tariff) -> Non
|
|||||||
|
|
||||||
# Tibber pricing must return the same value regardless of tariff.
|
# Tibber pricing must return the same value regardless of tariff.
|
||||||
for tariff_val in (1, 2, None):
|
for tariff_val in (1, 2, None):
|
||||||
set_current_tariff(tariff_val)
|
set_current_tariff(source_id, tariff_val)
|
||||||
with Session(energy_db) as session:
|
with Session(energy_db) as session:
|
||||||
catalog = build_catalog(session)
|
catalog = build_catalog(session)
|
||||||
buy_entry = next(e for e in catalog if e.entity.key == "energy.buy_price_now")
|
buy_entry = next(e for e in catalog if e.entity.key == "energy.buy_price_now")
|
||||||
@@ -1614,7 +1650,8 @@ def test_tibber_sell_price_not_affected_by_tariff(energy_db, reset_tariff) -> No
|
|||||||
}
|
}
|
||||||
|
|
||||||
with Session(energy_db) as session:
|
with Session(energy_db) as session:
|
||||||
_make_active_meter(session, started_at=meter_start)
|
meter = _make_active_meter(session, started_at=meter_start)
|
||||||
|
source_id = _bind_migrated_dsmr_source(session, meter, now=t0)
|
||||||
_make_period(
|
_make_period(
|
||||||
session,
|
session,
|
||||||
period_start=t0,
|
period_start=t0,
|
||||||
@@ -1624,7 +1661,7 @@ def test_tibber_sell_price_not_affected_by_tariff(energy_db, reset_tariff) -> No
|
|||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
for tariff_val in (1, 2, None):
|
for tariff_val in (1, 2, None):
|
||||||
set_current_tariff(tariff_val)
|
set_current_tariff(source_id, tariff_val)
|
||||||
with Session(energy_db) as session:
|
with Session(energy_db) as session:
|
||||||
catalog = build_catalog(session)
|
catalog = build_catalog(session)
|
||||||
sell_entry = next(e for e in catalog if e.entity.key == "energy.sell_price_now")
|
sell_entry = next(e for e in catalog if e.entity.key == "energy.sell_price_now")
|
||||||
@@ -2798,8 +2835,10 @@ def test_m8_meter_today_uses_current_binding_and_never_invents_zero(energy_db) -
|
|||||||
"""A binding is a half-open cumulative epoch, not merely a channel filter."""
|
"""A binding is a half-open cumulative epoch, not merely a channel filter."""
|
||||||
from app.integrations.expose import build_catalog
|
from app.integrations.expose import build_catalog
|
||||||
from app.models.meter_source import WarmteLinkReading
|
from app.models.meter_source import WarmteLinkReading
|
||||||
|
from app.services import timezone as tz
|
||||||
|
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime(2026, 1, 15, 10, tzinfo=timezone.utc)
|
||||||
|
local_tz = ZoneInfo("Europe/Amsterdam")
|
||||||
with Session(energy_db) as session:
|
with Session(energy_db) as session:
|
||||||
_source, channel, meter = _make_thermal_source_and_meter(session, "heating", now)
|
_source, channel, meter = _make_thermal_source_and_meter(session, "heating", now)
|
||||||
binding = meter.source_bindings[0]
|
binding = meter.source_bindings[0]
|
||||||
@@ -2816,15 +2855,20 @@ def test_m8_meter_today_uses_current_binding_and_never_invents_zero(energy_db) -
|
|||||||
equipment_fingerprint="test"),
|
equipment_fingerprint="test"),
|
||||||
))
|
))
|
||||||
session.commit()
|
session.commit()
|
||||||
entity = next(item.entity for item in build_catalog(session)
|
with (
|
||||||
if item.entity.key == f"meter.{meter.uuid}.today")
|
patch("app.integrations.expose._utc_now", return_value=now),
|
||||||
assert entity.value_getter(session) == Decimal("5")
|
patch.object(tz, "local_tz", return_value=local_tz),
|
||||||
session.query(WarmteLinkReading).filter(WarmteLinkReading.channel_id == channel.id).delete()
|
patch.object(tz, "local_now", return_value=now.astimezone(local_tz)),
|
||||||
session.add(WarmteLinkReading(channel_id=channel.id, recorded_at=now - timedelta(minutes=5),
|
):
|
||||||
received_at=now, value=Decimal("115"), unit="GJ", quality="valid",
|
entity = next(item.entity for item in build_catalog(session)
|
||||||
equipment_fingerprint="test"))
|
if item.entity.key == f"meter.{meter.uuid}.today")
|
||||||
session.flush()
|
assert entity.value_getter(session) == Decimal("5")
|
||||||
assert entity.value_getter(session) is None
|
session.query(WarmteLinkReading).filter(WarmteLinkReading.channel_id == channel.id).delete()
|
||||||
|
session.add(WarmteLinkReading(channel_id=channel.id, recorded_at=now - timedelta(minutes=5),
|
||||||
|
received_at=now, value=Decimal("115"), unit="GJ", quality="valid",
|
||||||
|
equipment_fingerprint="test"))
|
||||||
|
session.flush()
|
||||||
|
assert entity.value_getter(session) is None
|
||||||
|
|
||||||
|
|
||||||
def test_m8_dsmr_electricity_meter_reads_real_telegram_domain(energy_db) -> None:
|
def test_m8_dsmr_electricity_meter_reads_real_telegram_domain(energy_db) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user