"""add energy pricing and DSMR metering tables Revision ID: 20260623_11_energy_tables Revises: 20260622_10_exposed_entities Create Date: 2026-06-23 00:00:00.000000 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op revision: str = "20260623_11_energy_tables" down_revision: Union[str, None] = "20260622_10_exposed_entities" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # dsmr_reading — raw DSMR telegram blobs (10-second down-sampled). # No device table: single P1 smart meter; a ``source`` column can be added later # if a second meter is introduced. op.create_table( "dsmr_reading", sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), sa.Column("recorded_at", sa.DateTime(timezone=True), nullable=False), sa.Column("source_id", sa.Integer(), nullable=True), sa.Column("payload", sa.JSON(), nullable=False), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("source_id", name="uq_dsmr_reading_source_id"), ) op.create_index( "ix_dsmr_reading_recorded_at", "dsmr_reading", ["recorded_at"], unique=False, ) # energy_contract — contract head (manual or tibber, one active at a time). op.create_table( "energy_contract", sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), sa.Column("name", sa.String(length=255), nullable=False), sa.Column("kind", sa.String(length=32), nullable=False), sa.Column("active", sa.Boolean(), nullable=False), sa.Column("currency", sa.String(length=8), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), sa.PrimaryKeyConstraint("id"), ) # energy_contract_version — versioned pricing values; append-only for auditability. # Must be created after energy_contract because of the FK dependency. op.create_table( "energy_contract_version", sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), sa.Column("contract_id", sa.Integer(), nullable=False), sa.Column("effective_from", sa.DateTime(timezone=True), nullable=False), sa.Column("effective_to", sa.DateTime(timezone=True), nullable=True), sa.Column("values", sa.JSON(), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), sa.ForeignKeyConstraint( ["contract_id"], ["energy_contract.id"], ondelete="RESTRICT", ), sa.PrimaryKeyConstraint("id"), ) # tibber_price — cached Tibber 15-minute spot prices (immutable once fetched). op.create_table( "tibber_price", sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), sa.Column("starts_at", sa.DateTime(timezone=True), nullable=False), sa.Column("resolution", sa.String(length=32), nullable=False), sa.Column("energy", sa.Float(), nullable=False), sa.Column("tax", sa.Float(), nullable=False), sa.Column("total", sa.Float(), nullable=False), sa.Column("level", sa.String(length=32), nullable=True), sa.Column("currency", sa.String(length=8), nullable=False), sa.Column("fetched_at", sa.DateTime(timezone=True), nullable=False), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("starts_at", name="uq_tibber_price_starts_at"), ) # energy_cost_period — computed 15-minute billing periods (immutable snapshot). # Must be created after energy_contract_version because of the FK dependency. op.create_table( "energy_cost_period", sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), sa.Column("period_start", sa.DateTime(timezone=True), nullable=False), sa.Column("d1_kwh", sa.Float(), nullable=False), sa.Column("d2_kwh", sa.Float(), nullable=False), sa.Column("r1_kwh", sa.Float(), nullable=False), sa.Column("r2_kwh", sa.Float(), nullable=False), sa.Column("import_cost", sa.Float(), nullable=False), sa.Column("export_revenue", sa.Float(), nullable=False), sa.Column("net_cost", sa.Float(), nullable=False), sa.Column("currency", sa.String(length=8), nullable=False), sa.Column("pricing", sa.JSON(), nullable=False), sa.Column("contract_version_id", sa.Integer(), nullable=True), sa.Column("degraded", sa.Boolean(), nullable=False), sa.Column("computed_at", sa.DateTime(timezone=True), nullable=False), sa.ForeignKeyConstraint( ["contract_version_id"], ["energy_contract_version.id"], ondelete="RESTRICT", ), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("period_start", name="uq_energy_cost_period_period_start"), ) def downgrade() -> None: # Drop tables in reverse dependency order: tables with FKs first. # energy_cost_period has a FK to energy_contract_version — drop it first. op.drop_table("energy_cost_period") # tibber_price has no FK dependencies on our new tables. op.drop_table("tibber_price") # energy_contract_version has a FK to energy_contract — drop it before energy_contract. op.drop_table("energy_contract_version") # energy_contract has no FK dependencies on our new tables. op.drop_table("energy_contract") # dsmr_reading has no FK dependencies. op.drop_index("ix_dsmr_reading_recorded_at", table_name="dsmr_reading") op.drop_table("dsmr_reading")