133 lines
5.6 KiB
Python
133 lines
5.6 KiB
Python
"""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")
|