M8-T03: adopt DSMR history into meter sources
This commit is contained in:
+34
-10
@@ -13,8 +13,8 @@ from __future__ import annotations
|
||||
|
||||
import uuid as _uuid
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Boolean, DateTime, Float, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy import Boolean, DateTime, Float, ForeignKey, Integer, String, UniqueConstraint, event, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship, synonym
|
||||
from sqlalchemy.types import JSON
|
||||
|
||||
from app.db import Base
|
||||
@@ -97,8 +97,9 @@ class DsmrReading(Base):
|
||||
(that field overflows and must be manually reset to zero — a known DSMR
|
||||
quirk — so relying on it for uniqueness risks silently dropping new data).
|
||||
The table's own autoincrement ``id`` PK is the stable internal identity, and
|
||||
``recorded_at`` (the telegram timestamp) is the UNIQUE de-duplication key: a
|
||||
single P1 meter emits exactly one telegram per timestamp.
|
||||
``(meter_source_id, recorded_at)`` is the UNIQUE de-duplication key: each
|
||||
configured P1 source emits at most one telegram per timestamp, while
|
||||
different sources may legitimately emit at the same instant.
|
||||
|
||||
``recorded_at`` is a real column (not inside the payload) so time-range
|
||||
queries are efficient. The entire telegram frame is stored verbatim in
|
||||
@@ -110,21 +111,44 @@ class DsmrReading(Base):
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
# UTC timestamp of the sample — real column, UNIQUE (telegram-id-independent
|
||||
# idempotency key). The unique index also serves time-range queries.
|
||||
recorded_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, unique=True
|
||||
)
|
||||
# UTC timestamp of the sample. Idempotency is per configured source, so
|
||||
# distinct P1 sources may legitimately emit at the same instant.
|
||||
recorded_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
|
||||
# Telegram's own id (DSMR Reader assigns it). Stored only as a reference /
|
||||
# debugging aid — NOT used for uniqueness or idempotency (it overflows and
|
||||
# gets reset to zero). Nullable because some DSMR sources may not emit one.
|
||||
source_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
telegram_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
|
||||
# Compatibility for the pre-M8 ingest implementation. This is an ORM
|
||||
# alias only; the physical database column is ``telegram_id``.
|
||||
source_id = synonym("telegram_id")
|
||||
|
||||
# The configured source is the durable identity of the cumulative reading
|
||||
# stream. It is non-null after the revision-16 historical adoption.
|
||||
meter_source_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("meter_source.id", ondelete="RESTRICT"), nullable=False, index=True
|
||||
)
|
||||
|
||||
# Full telegram frame as a JSON object; values are typically JSON strings
|
||||
# (e.g. "20915.154") — callers must cast to Decimal before arithmetic.
|
||||
payload: Mapped[dict] = mapped_column(JSON, nullable=False)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"meter_source_id", "recorded_at", name="uq_dsmr_reading_source_recorded_at"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@event.listens_for(DsmrReading, "before_insert")
|
||||
def _supply_legacy_dsmr_source(_mapper, connection, target: DsmrReading) -> None:
|
||||
"""Keep the pre-T04 single-source writer working during the schema handoff."""
|
||||
if target.meter_source_id is None:
|
||||
target.meter_source_id = connection.execute(
|
||||
text("SELECT id FROM meter_source WHERE kind = 'dsmr_mqtt' ORDER BY id LIMIT 1")
|
||||
).scalar_one()
|
||||
|
||||
|
||||
class EnergyContract(Base):
|
||||
"""Contract head: a named energy contract with a chosen pricing strategy.
|
||||
|
||||
Reference in New Issue
Block a user