Config hot-reload (no restart): - MqttManager.unsubscribe(); apply_dsmr_subscription(settings) re-applies the DSMR subscription (subscribe/unsubscribe/topic-change, fresh snapshot so the sample interval also takes effect). - Called from lifespan AND PUT /api/config, so toggling DSMR ingest in the UI takes effect immediately without restarting the app. Decouple ingest from the DSMR telegram id (overflows / resets to zero): - Migration 20260624_12: drop UNIQUE(source_id), make recorded_at UNIQUE. - Idempotency now keys on recorded_at (telegram timestamp); the table's own autoincrement PK is the stable identity. source_id kept only as a reference. - Regression test: colliding telegram ids no longer drop new data.
59 lines
2.5 KiB
Python
59 lines
2.5 KiB
Python
"""decouple dsmr_reading from the telegram id
|
|
|
|
The DSMR Reader's own ``id`` field is auto-incrementing but is known to overflow
|
|
and require a manual reset to zero (a long-standing DSMR firmware quirk). If we
|
|
keep a UNIQUE constraint on ``source_id`` (the telegram id) and use it for
|
|
idempotency, an overflow/reset would make legitimately-new telegrams collide with
|
|
old ids and be silently dropped as "duplicates" — data loss.
|
|
|
|
This migration removes that coupling:
|
|
|
|
- Drops the UNIQUE constraint on ``source_id`` (the column is kept as a plain,
|
|
nullable reference value; it is no longer relied upon for uniqueness/dedup).
|
|
- Makes ``recorded_at`` (the telegram timestamp) UNIQUE instead — a single P1
|
|
meter emits one telegram per timestamp, so this is a robust, telegram-id-
|
|
independent idempotency key. The table's own autoincrement ``id`` PK remains
|
|
the stable internal identity.
|
|
|
|
Revision ID: 20260624_12_dsmr_decouple_telegram_id
|
|
Revises: 20260623_11_energy_tables
|
|
Create Date: 2026-06-24 00:00:00.000000
|
|
"""
|
|
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
|
|
|
|
revision: str = "20260624_12_dsmr_decouple_telegram_id"
|
|
down_revision: Union[str, None] = "20260623_11_energy_tables"
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
# SQLite cannot ALTER away a constraint in place, so recreate the table via
|
|
# Alembic's batch mode. The table is recreated and rows are copied; existing
|
|
# data (if any) is preserved. recorded_at must be unique for this to succeed
|
|
# — a single meter never emits two telegrams at the exact same timestamp.
|
|
with op.batch_alter_table("dsmr_reading", schema=None) as batch_op:
|
|
# Old non-unique index on recorded_at is replaced by the unique constraint.
|
|
batch_op.drop_index("ix_dsmr_reading_recorded_at")
|
|
# Telegram id is no longer a uniqueness/idempotency key.
|
|
batch_op.drop_constraint("uq_dsmr_reading_source_id", type_="unique")
|
|
# Timestamp becomes the telegram-id-independent dedup key.
|
|
batch_op.create_unique_constraint(
|
|
"uq_dsmr_reading_recorded_at", ["recorded_at"]
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
with op.batch_alter_table("dsmr_reading", schema=None) as batch_op:
|
|
batch_op.drop_constraint("uq_dsmr_reading_recorded_at", type_="unique")
|
|
batch_op.create_unique_constraint(
|
|
"uq_dsmr_reading_source_id", ["source_id"]
|
|
)
|
|
batch_op.create_index(
|
|
"ix_dsmr_reading_recorded_at", ["recorded_at"], unique=False
|
|
)
|