fix(models): add alembic migration 009 for missing tables (v1.4.1)
Migration adds tables that existed in models/ but were never included in alembic history: - ai_ops: device_health_scores, anomaly_detections, diagnostic_reports, maintenance_predictions, ops_insights - energy_strategy: tou_pricing, tou_pricing_periods, energy_strategies, strategy_executions, monthly_cost_reports - weather: weather_data, weather_config - prediction: prediction_tasks, prediction_results, optimization_schedules Without this migration, fresh deploys would 500 on these endpoints: - /api/v1/ai-ops/health, /ai-ops/dashboard - /api/v1/strategy/pricing - /api/v1/prediction/forecast - /api/v1/weather/current Discovered during Z-Park demo deployment on xie_openclaw1. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
"""Add ai_ops, energy_strategy, weather, prediction tables
|
||||
|
||||
Revision ID: 009_aiops_strategy
|
||||
Revises: 008_management
|
||||
Create Date: 2026-04-10
|
||||
|
||||
Adds tables for features that were previously missing from alembic history:
|
||||
- AI Ops: device_health_scores, anomaly_detections, diagnostic_reports,
|
||||
maintenance_predictions, ops_insights
|
||||
- Energy Strategy: tou_pricing, tou_pricing_periods, energy_strategies,
|
||||
strategy_executions, monthly_cost_reports
|
||||
- Weather: weather_data, weather_config
|
||||
- Prediction: prediction_tasks, prediction_results, optimization_schedules
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "009_aiops_strategy"
|
||||
down_revision = "008_management"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# =========================================================================
|
||||
# AI Ops tables
|
||||
# =========================================================================
|
||||
op.create_table(
|
||||
"device_health_scores",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("device_id", sa.Integer, sa.ForeignKey("devices.id"), nullable=False),
|
||||
sa.Column("timestamp", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.Column("health_score", sa.Float, nullable=False),
|
||||
sa.Column("status", sa.String(20), default="healthy"),
|
||||
sa.Column("factors", sa.JSON),
|
||||
sa.Column("trend", sa.String(20), default="stable"),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("ix_device_health_scores_device_id", "device_health_scores", ["device_id"])
|
||||
op.create_index("ix_device_health_scores_timestamp", "device_health_scores", ["timestamp"])
|
||||
|
||||
op.create_table(
|
||||
"anomaly_detections",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("device_id", sa.Integer, sa.ForeignKey("devices.id"), nullable=False),
|
||||
sa.Column("detected_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.Column("anomaly_type", sa.String(50), nullable=False),
|
||||
sa.Column("severity", sa.String(20), default="warning"),
|
||||
sa.Column("description", sa.Text),
|
||||
sa.Column("metric_name", sa.String(50)),
|
||||
sa.Column("expected_value", sa.Float),
|
||||
sa.Column("actual_value", sa.Float),
|
||||
sa.Column("deviation_percent", sa.Float),
|
||||
sa.Column("status", sa.String(20), default="detected"),
|
||||
sa.Column("resolution_notes", sa.Text),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("ix_anomaly_detections_device_id", "anomaly_detections", ["device_id"])
|
||||
op.create_index("ix_anomaly_detections_detected_at", "anomaly_detections", ["detected_at"])
|
||||
|
||||
op.create_table(
|
||||
"diagnostic_reports",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("device_id", sa.Integer, sa.ForeignKey("devices.id"), nullable=False),
|
||||
sa.Column("generated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.Column("report_type", sa.String(20), default="routine"),
|
||||
sa.Column("findings", sa.JSON),
|
||||
sa.Column("recommendations", sa.JSON),
|
||||
sa.Column("estimated_impact", sa.JSON),
|
||||
sa.Column("status", sa.String(20), default="generated"),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("ix_diagnostic_reports_device_id", "diagnostic_reports", ["device_id"])
|
||||
|
||||
op.create_table(
|
||||
"maintenance_predictions",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("device_id", sa.Integer, sa.ForeignKey("devices.id"), nullable=False),
|
||||
sa.Column("predicted_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.Column("component", sa.String(100)),
|
||||
sa.Column("failure_mode", sa.String(200)),
|
||||
sa.Column("probability", sa.Float),
|
||||
sa.Column("predicted_failure_date", sa.DateTime(timezone=True)),
|
||||
sa.Column("recommended_action", sa.Text),
|
||||
sa.Column("urgency", sa.String(20), default="medium"),
|
||||
sa.Column("estimated_downtime_hours", sa.Float),
|
||||
sa.Column("estimated_repair_cost", sa.Float),
|
||||
sa.Column("status", sa.String(20), default="predicted"),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("ix_maintenance_predictions_device_id", "maintenance_predictions", ["device_id"])
|
||||
|
||||
op.create_table(
|
||||
"ops_insights",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("insight_type", sa.String(50), nullable=False),
|
||||
sa.Column("title", sa.String(200), nullable=False),
|
||||
sa.Column("description", sa.Text),
|
||||
sa.Column("data", sa.JSON),
|
||||
sa.Column("impact_level", sa.String(20), default="medium"),
|
||||
sa.Column("actionable", sa.Boolean, default=False),
|
||||
sa.Column("recommended_action", sa.Text),
|
||||
sa.Column("generated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.Column("valid_until", sa.DateTime(timezone=True)),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# Energy Strategy tables
|
||||
# =========================================================================
|
||||
op.create_table(
|
||||
"tou_pricing",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("name", sa.String(200), nullable=False),
|
||||
sa.Column("region", sa.String(100), default="北京"),
|
||||
sa.Column("effective_date", sa.Date),
|
||||
sa.Column("end_date", sa.Date),
|
||||
sa.Column("is_active", sa.Boolean, default=True),
|
||||
sa.Column("created_by", sa.Integer, sa.ForeignKey("users.id")),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"tou_pricing_periods",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("pricing_id", sa.Integer, sa.ForeignKey("tou_pricing.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("period_type", sa.String(20), nullable=False),
|
||||
sa.Column("start_time", sa.String(10), nullable=False),
|
||||
sa.Column("end_time", sa.String(10), nullable=False),
|
||||
sa.Column("price_yuan_per_kwh", sa.Float, nullable=False),
|
||||
sa.Column("month_range", sa.String(50)),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"energy_strategies",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("name", sa.String(200), nullable=False),
|
||||
sa.Column("strategy_type", sa.String(50), nullable=False),
|
||||
sa.Column("description", sa.String(500)),
|
||||
sa.Column("parameters", sa.JSON),
|
||||
sa.Column("is_enabled", sa.Boolean, default=False),
|
||||
sa.Column("priority", sa.Integer, default=0),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"strategy_executions",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("strategy_id", sa.Integer, sa.ForeignKey("energy_strategies.id"), nullable=False),
|
||||
sa.Column("date", sa.Date, nullable=False),
|
||||
sa.Column("actions_taken", sa.JSON),
|
||||
sa.Column("savings_kwh", sa.Float, default=0),
|
||||
sa.Column("savings_yuan", sa.Float, default=0),
|
||||
sa.Column("status", sa.String(20), default="planned"),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"monthly_cost_reports",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("year_month", sa.String(7), nullable=False, unique=True),
|
||||
sa.Column("total_consumption_kwh", sa.Float, default=0),
|
||||
sa.Column("total_cost_yuan", sa.Float, default=0),
|
||||
sa.Column("peak_consumption", sa.Float, default=0),
|
||||
sa.Column("valley_consumption", sa.Float, default=0),
|
||||
sa.Column("flat_consumption", sa.Float, default=0),
|
||||
sa.Column("sharp_peak_consumption", sa.Float, default=0),
|
||||
sa.Column("pv_self_consumption", sa.Float, default=0),
|
||||
sa.Column("pv_feed_in", sa.Float, default=0),
|
||||
sa.Column("optimized_cost", sa.Float, default=0),
|
||||
sa.Column("baseline_cost", sa.Float, default=0),
|
||||
sa.Column("savings_yuan", sa.Float, default=0),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# Weather tables
|
||||
# =========================================================================
|
||||
op.create_table(
|
||||
"weather_data",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("data_type", sa.String(20), nullable=False),
|
||||
sa.Column("temperature", sa.Float),
|
||||
sa.Column("humidity", sa.Float),
|
||||
sa.Column("solar_radiation", sa.Float),
|
||||
sa.Column("cloud_cover", sa.Float),
|
||||
sa.Column("wind_speed", sa.Float),
|
||||
sa.Column("source", sa.String(20), default="mock"),
|
||||
sa.Column("fetched_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("ix_weather_data_timestamp", "weather_data", ["timestamp"])
|
||||
|
||||
op.create_table(
|
||||
"weather_config",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("api_provider", sa.String(50), default="mock"),
|
||||
sa.Column("api_key", sa.String(200)),
|
||||
sa.Column("location_lat", sa.Float, default=39.9),
|
||||
sa.Column("location_lon", sa.Float, default=116.4),
|
||||
sa.Column("fetch_interval_minutes", sa.Integer, default=30),
|
||||
sa.Column("is_enabled", sa.Boolean, default=True),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# Prediction tables
|
||||
# =========================================================================
|
||||
op.create_table(
|
||||
"prediction_tasks",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("device_id", sa.Integer, sa.ForeignKey("devices.id")),
|
||||
sa.Column("prediction_type", sa.String(50), nullable=False),
|
||||
sa.Column("horizon_hours", sa.Integer, default=24),
|
||||
sa.Column("status", sa.String(20), default="pending"),
|
||||
sa.Column("parameters", sa.JSON),
|
||||
sa.Column("error_message", sa.Text),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
sa.Column("completed_at", sa.DateTime(timezone=True)),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"prediction_results",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("task_id", sa.Integer, sa.ForeignKey("prediction_tasks.id"), nullable=False),
|
||||
sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("predicted_value", sa.Float, nullable=False),
|
||||
sa.Column("confidence_lower", sa.Float),
|
||||
sa.Column("confidence_upper", sa.Float),
|
||||
sa.Column("actual_value", sa.Float),
|
||||
sa.Column("unit", sa.String(20)),
|
||||
)
|
||||
op.create_index("ix_prediction_results_task_id", "prediction_results", ["task_id"])
|
||||
op.create_index("ix_prediction_results_timestamp", "prediction_results", ["timestamp"])
|
||||
|
||||
op.create_table(
|
||||
"optimization_schedules",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("device_id", sa.Integer, sa.ForeignKey("devices.id")),
|
||||
sa.Column("date", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("schedule_data", sa.JSON),
|
||||
sa.Column("expected_savings_kwh", sa.Float, default=0),
|
||||
sa.Column("expected_savings_yuan", sa.Float, default=0),
|
||||
sa.Column("status", sa.String(20), default="pending"),
|
||||
sa.Column("approved_by", sa.Integer, sa.ForeignKey("users.id")),
|
||||
sa.Column("approved_at", sa.DateTime(timezone=True)),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("ix_optimization_schedules_date", "optimization_schedules", ["date"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Prediction
|
||||
op.drop_index("ix_optimization_schedules_date", table_name="optimization_schedules")
|
||||
op.drop_table("optimization_schedules")
|
||||
op.drop_index("ix_prediction_results_timestamp", table_name="prediction_results")
|
||||
op.drop_index("ix_prediction_results_task_id", table_name="prediction_results")
|
||||
op.drop_table("prediction_results")
|
||||
op.drop_table("prediction_tasks")
|
||||
|
||||
# Weather
|
||||
op.drop_table("weather_config")
|
||||
op.drop_index("ix_weather_data_timestamp", table_name="weather_data")
|
||||
op.drop_table("weather_data")
|
||||
|
||||
# Energy Strategy
|
||||
op.drop_table("monthly_cost_reports")
|
||||
op.drop_table("strategy_executions")
|
||||
op.drop_table("energy_strategies")
|
||||
op.drop_table("tou_pricing_periods")
|
||||
op.drop_table("tou_pricing")
|
||||
|
||||
# AI Ops
|
||||
op.drop_table("ops_insights")
|
||||
op.drop_index("ix_maintenance_predictions_device_id", table_name="maintenance_predictions")
|
||||
op.drop_table("maintenance_predictions")
|
||||
op.drop_index("ix_diagnostic_reports_device_id", table_name="diagnostic_reports")
|
||||
op.drop_table("diagnostic_reports")
|
||||
op.drop_index("ix_anomaly_detections_detected_at", table_name="anomaly_detections")
|
||||
op.drop_index("ix_anomaly_detections_device_id", table_name="anomaly_detections")
|
||||
op.drop_table("anomaly_detections")
|
||||
op.drop_index("ix_device_health_scores_timestamp", table_name="device_health_scores")
|
||||
op.drop_index("ix_device_health_scores_device_id", table_name="device_health_scores")
|
||||
op.drop_table("device_health_scores")
|
||||
Reference in New Issue
Block a user