feat: complete platform build-out to 95% benchmark-ready
Major additions across backend, frontend, and infrastructure: Backend: - IoT collector framework (Modbus TCP, MQTT, HTTP) with manager - Realistic Beijing solar/weather simulator with cloud transients - Alarm auto-checker with demo anomaly injection (3-4 events/hour) - Report generation (PDF/Excel) with sync fallback and E2E testing - Energy data CSV/XLSX export endpoint - WebSocket real-time broadcast at /ws/realtime - Alembic initial migration for all 14 tables - 77 pytest tests across 9 API routers Frontend: - Live notification badge with alarm count (was hardcoded 0) - Sankey energy flow diagram on dashboard - Device photos (SVG illustrations) on all device pages - Report download with status icons - Energy data export buttons (CSV/Excel) - WebSocket hook with auto-reconnect and polling fallback - BigScreen 2D responsive CSS (tablet/mobile) - Error handling improvements across pages Infrastructure: - PostgreSQL + TimescaleDB as primary database - Production docker-compose with nginx reverse proxy - Comprehensive Chinese README - .env.example with documentation - quick-start.sh deployment script - nginx config with gzip, caching, security headers Data: - 30-day realistic backfill (47K rows, weather-correlated) - 18 devices, 6 alarm rules, 15 historical alarm events - Beijing solar position model with seasonal variation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
"""回填历史模拟能耗数据 - 过去30天逐小时数据"""
|
||||
"""回填历史模拟能耗数据 - 过去30天逐小时数据,含碳排放记录
|
||||
|
||||
Uses the shared weather_model for physics-based solar, temperature, and load
|
||||
generation. Deterministic seed (42) ensures reproducible output across runs.
|
||||
"""
|
||||
import asyncio
|
||||
import math
|
||||
import os
|
||||
@@ -6,70 +10,57 @@ import random
|
||||
import sys
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
sys.path.insert(0, "../backend")
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "backend"))
|
||||
|
||||
# Allow override via env var (same pattern as other scripts)
|
||||
DATABASE_URL = os.environ.get(
|
||||
"DATABASE_URL",
|
||||
"postgresql+asyncpg://tianpu:tianpu2026@localhost:5432/tianpu_ems",
|
||||
)
|
||||
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy import text, select
|
||||
|
||||
from app.services.weather_model import (
|
||||
set_seed, reset_cloud_model,
|
||||
pv_power, pv_electrical_at, get_pv_orientation,
|
||||
heat_pump_data, building_load, indoor_sensor,
|
||||
heat_meter_data, outdoor_temperature, outdoor_humidity,
|
||||
get_hvac_mode,
|
||||
)
|
||||
from app.models.device import Device
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Device definitions
|
||||
# Device definitions — will be populated from DB at runtime
|
||||
# ---------------------------------------------------------------------------
|
||||
PV_IDS = [1, 2, 3] # 110 kW rated each
|
||||
HP_IDS = [4, 5, 6, 7] # 35 kW rated each
|
||||
METER_IDS = [8, 9, 10, 11]
|
||||
PV_IDS = []
|
||||
PV_CODES = ["INV-01", "INV-02", "INV-03"]
|
||||
HP_IDS = []
|
||||
HP_CODES = ["HP-01", "HP-02", "HP-03", "HP-04"]
|
||||
METER_IDS = []
|
||||
METER_CODES = ["METER-GRID", "METER-PV", "METER-HP", "METER-PUMP"]
|
||||
HEAT_METER_ID = None
|
||||
SENSOR_IDS = []
|
||||
SENSOR_CODES = ["TH-01", "TH-02", "TH-03", "TH-04", "TH-05"]
|
||||
|
||||
|
||||
async def _load_device_ids(session: AsyncSession):
|
||||
"""Load actual device IDs from DB by code."""
|
||||
global PV_IDS, HP_IDS, METER_IDS, HEAT_METER_ID, SENSOR_IDS
|
||||
result = await session.execute(select(Device.id, Device.code).order_by(Device.id))
|
||||
code_to_id = {row[1]: row[0] for row in result.all()}
|
||||
|
||||
PV_IDS = [code_to_id[c] for c in PV_CODES if c in code_to_id]
|
||||
HP_IDS = [code_to_id[c] for c in HP_CODES if c in code_to_id]
|
||||
METER_IDS = [code_to_id[c] for c in METER_CODES if c in code_to_id]
|
||||
HEAT_METER_ID = code_to_id.get("HM-01")
|
||||
SENSOR_IDS = [code_to_id[c] for c in SENSOR_CODES if c in code_to_id]
|
||||
print(f" Loaded device IDs: PV={PV_IDS}, HP={HP_IDS}, Meters={METER_IDS}, HeatMeter={HEAT_METER_ID}, Sensors={SENSOR_IDS}")
|
||||
|
||||
EMISSION_FACTOR = 0.8843 # kgCO2/kWh - North China grid
|
||||
|
||||
DAYS = 30
|
||||
HOURS_PER_DAY = 24
|
||||
TOTAL_HOURS = DAYS * HOURS_PER_DAY # 720
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Curve generators (return kW for a given hour-of-day 0-23)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def pv_power(hour: int, rated: float = 110.0) -> float:
|
||||
"""Solar bell curve: 0 at night, peak ~80-100 kW around noon."""
|
||||
if hour < 6 or hour >= 18:
|
||||
return 0.0
|
||||
# sine curve from 6-18 with peak at 12
|
||||
x = (hour - 6) / 12.0 * math.pi
|
||||
base = math.sin(x) * rated * random.uniform(0.72, 0.92)
|
||||
noise = base * random.uniform(-0.15, 0.15)
|
||||
return max(0.0, base + noise)
|
||||
|
||||
|
||||
def heat_pump_power(hour: int, rated: float = 35.0) -> float:
|
||||
"""Heat pump load: base 15-25 kW, peaks morning 7-9 and evening 17-20.
|
||||
Spring season so moderate."""
|
||||
base = random.uniform(15, 25)
|
||||
if 7 <= hour <= 9:
|
||||
base += random.uniform(5, 10)
|
||||
elif 17 <= hour <= 20:
|
||||
base += random.uniform(5, 10)
|
||||
elif 0 <= hour <= 5:
|
||||
base -= random.uniform(3, 8)
|
||||
base = min(base, rated)
|
||||
noise = base * random.uniform(-0.20, 0.20)
|
||||
return max(0.0, base + noise)
|
||||
|
||||
|
||||
def meter_power(hour: int) -> float:
|
||||
"""Building load: ~60 kW at night, ~100 kW during business hours."""
|
||||
if 8 <= hour <= 18:
|
||||
base = random.uniform(85, 115)
|
||||
elif 6 <= hour <= 7 or 19 <= hour <= 21:
|
||||
base = random.uniform(65, 85)
|
||||
else:
|
||||
base = random.uniform(45, 70)
|
||||
noise = base * random.uniform(-0.15, 0.15)
|
||||
return max(0.0, base + noise)
|
||||
TOTAL_HOURS = DAYS * HOURS_PER_DAY
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -77,65 +68,177 @@ def meter_power(hour: int) -> float:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def backfill():
|
||||
# Set deterministic seed for reproducibility
|
||||
set_seed(42)
|
||||
|
||||
engine = create_async_engine(DATABASE_URL, echo=False, pool_size=5)
|
||||
session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
# Load actual device IDs from DB
|
||||
async with session_factory() as session:
|
||||
await _load_device_ids(session)
|
||||
|
||||
now = datetime.now(timezone.utc).replace(minute=0, second=0, microsecond=0)
|
||||
start = now - timedelta(days=DAYS)
|
||||
|
||||
print(f"Backfill range: {start.isoformat()} -> {now.isoformat()}")
|
||||
print(f"Total hours: {TOTAL_HOURS}")
|
||||
|
||||
# ---- Collect hourly energy_data rows and per-device daily buckets ----
|
||||
energy_rows = [] # list of dicts for bulk insert
|
||||
# daily_buckets[device_id][date_str] = list of hourly values
|
||||
daily_buckets: dict[int, dict[str, list[float]]] = {}
|
||||
# ---- Collect rows ----
|
||||
energy_rows = []
|
||||
carbon_rows = []
|
||||
daily_buckets: dict[int, dict[str, dict]] = {}
|
||||
|
||||
all_device_ids = PV_IDS + HP_IDS + METER_IDS
|
||||
for did in all_device_ids:
|
||||
all_power_ids = PV_IDS + HP_IDS + METER_IDS
|
||||
for did in all_power_ids:
|
||||
daily_buckets[did] = {}
|
||||
|
||||
print("Generating hourly energy_data rows ...")
|
||||
print("Generating hourly energy_data rows (realistic models) ...")
|
||||
for h_offset in range(TOTAL_HOURS):
|
||||
ts = start + timedelta(hours=h_offset)
|
||||
hour = ts.hour
|
||||
date_str = ts.strftime("%Y-%m-%d")
|
||||
beijing_dt = ts + timedelta(hours=8)
|
||||
date_str = beijing_dt.strftime("%Y-%m-%d")
|
||||
|
||||
for did in PV_IDS:
|
||||
val = round(pv_power(hour), 2)
|
||||
energy_rows.append({
|
||||
"device_id": did,
|
||||
"timestamp": ts,
|
||||
"data_type": "power",
|
||||
"value": val,
|
||||
"unit": "kW",
|
||||
"quality": 100,
|
||||
})
|
||||
daily_buckets[did].setdefault(date_str, []).append(val)
|
||||
# Reset cloud model each day for variety
|
||||
if h_offset % 24 == 0:
|
||||
reset_cloud_model()
|
||||
# Re-seed per day for reproducibility but day-to-day variation
|
||||
set_seed(42 + h_offset // 24)
|
||||
|
||||
for did in HP_IDS:
|
||||
val = round(heat_pump_power(hour), 2)
|
||||
energy_rows.append({
|
||||
"device_id": did,
|
||||
"timestamp": ts,
|
||||
"data_type": "power",
|
||||
"value": val,
|
||||
"unit": "kW",
|
||||
"quality": 100,
|
||||
})
|
||||
daily_buckets[did].setdefault(date_str, []).append(val)
|
||||
# --- PV inverters ---
|
||||
for i, did in enumerate(PV_IDS):
|
||||
code = PV_CODES[i]
|
||||
orientation = get_pv_orientation(code)
|
||||
val = pv_power(ts, rated_power=110.0, orientation=orientation,
|
||||
device_code=code)
|
||||
val = round(val, 2)
|
||||
|
||||
for did in METER_IDS:
|
||||
val = round(meter_power(hour), 2)
|
||||
energy_rows.append({
|
||||
"device_id": did,
|
||||
"timestamp": ts,
|
||||
"data_type": "power",
|
||||
"value": val,
|
||||
"unit": "kW",
|
||||
"quality": 100,
|
||||
"device_id": did, "timestamp": ts,
|
||||
"data_type": "power", "value": val, "unit": "kW", "quality": 0,
|
||||
})
|
||||
|
||||
# Also generate electrical details for richer data
|
||||
elec = pv_electrical_at(val, ts, rated_power=110.0)
|
||||
energy_rows.append({
|
||||
"device_id": did, "timestamp": ts,
|
||||
"data_type": "dc_voltage", "value": elec["dc_voltage"], "unit": "V", "quality": 0,
|
||||
})
|
||||
energy_rows.append({
|
||||
"device_id": did, "timestamp": ts,
|
||||
"data_type": "ac_voltage", "value": elec["ac_voltage"], "unit": "V", "quality": 0,
|
||||
})
|
||||
energy_rows.append({
|
||||
"device_id": did, "timestamp": ts,
|
||||
"data_type": "temperature", "value": elec["temperature"], "unit": "℃", "quality": 0,
|
||||
})
|
||||
|
||||
daily_buckets[did].setdefault(date_str, {"values": [], "cops": []})
|
||||
daily_buckets[did][date_str]["values"].append(val)
|
||||
|
||||
# --- Heat pumps ---
|
||||
hp_total_power = 0.0
|
||||
hp_cop_sum = 0.0
|
||||
hp_count = 0
|
||||
for i, did in enumerate(HP_IDS):
|
||||
code = HP_CODES[i]
|
||||
data = heat_pump_data(ts, rated_power=35.0, device_code=code)
|
||||
val = data["power"]
|
||||
cop = data["cop"]
|
||||
|
||||
hp_total_power += val
|
||||
if cop > 0:
|
||||
hp_cop_sum += cop
|
||||
hp_count += 1
|
||||
|
||||
energy_rows.append({
|
||||
"device_id": did, "timestamp": ts,
|
||||
"data_type": "power", "value": val, "unit": "kW", "quality": 0,
|
||||
})
|
||||
energy_rows.append({
|
||||
"device_id": did, "timestamp": ts,
|
||||
"data_type": "cop", "value": cop, "unit": "", "quality": 0,
|
||||
})
|
||||
energy_rows.append({
|
||||
"device_id": did, "timestamp": ts,
|
||||
"data_type": "inlet_temp", "value": data["inlet_temp"], "unit": "℃", "quality": 0,
|
||||
})
|
||||
energy_rows.append({
|
||||
"device_id": did, "timestamp": ts,
|
||||
"data_type": "outlet_temp", "value": data["outlet_temp"], "unit": "℃", "quality": 0,
|
||||
})
|
||||
energy_rows.append({
|
||||
"device_id": did, "timestamp": ts,
|
||||
"data_type": "flow_rate", "value": data["flow_rate"], "unit": "m³/h", "quality": 0,
|
||||
})
|
||||
energy_rows.append({
|
||||
"device_id": did, "timestamp": ts,
|
||||
"data_type": "outdoor_temp", "value": data["outdoor_temp"], "unit": "℃", "quality": 0,
|
||||
})
|
||||
|
||||
daily_buckets[did].setdefault(date_str, {"values": [], "cops": []})
|
||||
daily_buckets[did][date_str]["values"].append(val)
|
||||
daily_buckets[did][date_str]["cops"].append(cop)
|
||||
|
||||
# --- Meters ---
|
||||
for i, did in enumerate(METER_IDS):
|
||||
code = METER_CODES[i]
|
||||
data = building_load(ts, base_power=50.0, meter_code=code)
|
||||
val = data["power"]
|
||||
|
||||
energy_rows.append({
|
||||
"device_id": did, "timestamp": ts,
|
||||
"data_type": "power", "value": val, "unit": "kW", "quality": 0,
|
||||
})
|
||||
energy_rows.append({
|
||||
"device_id": did, "timestamp": ts,
|
||||
"data_type": "voltage", "value": data["voltage"], "unit": "V", "quality": 0,
|
||||
})
|
||||
energy_rows.append({
|
||||
"device_id": did, "timestamp": ts,
|
||||
"data_type": "current", "value": data["current"], "unit": "A", "quality": 0,
|
||||
})
|
||||
energy_rows.append({
|
||||
"device_id": did, "timestamp": ts,
|
||||
"data_type": "power_factor", "value": data["power_factor"], "unit": "", "quality": 0,
|
||||
})
|
||||
|
||||
daily_buckets[did].setdefault(date_str, {"values": [], "cops": []})
|
||||
daily_buckets[did][date_str]["values"].append(val)
|
||||
|
||||
# --- Heat meter (correlated with heat pump totals) ---
|
||||
avg_cop = hp_cop_sum / hp_count if hp_count > 0 else 3.0
|
||||
hm_data = heat_meter_data(ts, hp_power=hp_total_power, hp_cop=avg_cop)
|
||||
energy_rows.append({
|
||||
"device_id": HEAT_METER_ID, "timestamp": ts,
|
||||
"data_type": "heat_power", "value": hm_data["heat_power"], "unit": "kW", "quality": 0,
|
||||
})
|
||||
energy_rows.append({
|
||||
"device_id": HEAT_METER_ID, "timestamp": ts,
|
||||
"data_type": "flow_rate", "value": hm_data["flow_rate"], "unit": "m³/h", "quality": 0,
|
||||
})
|
||||
energy_rows.append({
|
||||
"device_id": HEAT_METER_ID, "timestamp": ts,
|
||||
"data_type": "supply_temp", "value": hm_data["supply_temp"], "unit": "℃", "quality": 0,
|
||||
})
|
||||
energy_rows.append({
|
||||
"device_id": HEAT_METER_ID, "timestamp": ts,
|
||||
"data_type": "return_temp", "value": hm_data["return_temp"], "unit": "℃", "quality": 0,
|
||||
})
|
||||
|
||||
# --- Temperature/humidity sensors ---
|
||||
for i, sid in enumerate(SENSOR_IDS):
|
||||
code = SENSOR_CODES[i]
|
||||
is_outdoor = (code == "TH-05")
|
||||
data = indoor_sensor(ts, is_outdoor=is_outdoor, device_code=code)
|
||||
energy_rows.append({
|
||||
"device_id": sid, "timestamp": ts,
|
||||
"data_type": "temperature", "value": data["temperature"], "unit": "℃", "quality": 0,
|
||||
})
|
||||
energy_rows.append({
|
||||
"device_id": sid, "timestamp": ts,
|
||||
"data_type": "humidity", "value": data["humidity"], "unit": "%", "quality": 0,
|
||||
})
|
||||
daily_buckets[did].setdefault(date_str, []).append(val)
|
||||
|
||||
print(f" Generated {len(energy_rows)} energy_data rows")
|
||||
|
||||
@@ -144,14 +247,17 @@ async def backfill():
|
||||
summary_rows = []
|
||||
for did, dates in daily_buckets.items():
|
||||
is_pv = did in PV_IDS
|
||||
for date_str, values in dates.items():
|
||||
total = round(sum(values), 2) # kWh (hourly power * 1h)
|
||||
peak = round(max(values), 2)
|
||||
min_p = round(min(values), 2)
|
||||
avg_p = round(sum(values) / len(values), 2)
|
||||
op_hours = sum(1 for v in values if v > 0)
|
||||
for date_str, bucket in dates.items():
|
||||
values = bucket["values"]
|
||||
cops = bucket["cops"]
|
||||
total = round(sum(values), 2)
|
||||
peak = round(max(values), 2) if values else 0
|
||||
min_p = round(min(values), 2) if values else 0
|
||||
avg_p = round(sum(values) / len(values), 2) if values else 0
|
||||
op_hours = sum(1 for v in values if v > 0.5)
|
||||
cost = round(total * 0.85, 2)
|
||||
carbon = round(total * 0.8843, 2)
|
||||
carbon = round(total * EMISSION_FACTOR, 2)
|
||||
avg_cop = round(sum(cops) / len(cops), 2) if cops else None
|
||||
|
||||
summary_rows.append({
|
||||
"device_id": did,
|
||||
@@ -163,12 +269,75 @@ async def backfill():
|
||||
"min_power": min_p,
|
||||
"avg_power": avg_p,
|
||||
"operating_hours": float(op_hours),
|
||||
"avg_cop": avg_cop,
|
||||
"cost": cost,
|
||||
"carbon_emission": carbon,
|
||||
})
|
||||
|
||||
print(f" Generated {len(summary_rows)} daily summary rows")
|
||||
|
||||
# ---- Build carbon emission daily rows ----
|
||||
print("Computing daily carbon emissions ...")
|
||||
daily_consumption: dict[str, float] = {}
|
||||
daily_pv_gen: dict[str, float] = {}
|
||||
daily_hp_consumption: dict[str, float] = {}
|
||||
|
||||
for did, dates in daily_buckets.items():
|
||||
for date_str, bucket in dates.items():
|
||||
total = sum(bucket["values"])
|
||||
if did in PV_IDS:
|
||||
daily_pv_gen[date_str] = daily_pv_gen.get(date_str, 0) + total
|
||||
elif did in HP_IDS:
|
||||
daily_hp_consumption[date_str] = daily_hp_consumption.get(date_str, 0) + total
|
||||
daily_consumption[date_str] = daily_consumption.get(date_str, 0) + total
|
||||
else:
|
||||
daily_consumption[date_str] = daily_consumption.get(date_str, 0) + total
|
||||
|
||||
all_dates = sorted(set(list(daily_consumption.keys()) + list(daily_pv_gen.keys())))
|
||||
for date_str in all_dates:
|
||||
dt = datetime.strptime(date_str, "%Y-%m-%d").replace(tzinfo=timezone.utc)
|
||||
|
||||
# Grid electricity emission (Scope 2)
|
||||
grid_kwh = daily_consumption.get(date_str, 0)
|
||||
carbon_rows.append({
|
||||
"date": dt, "scope": 2, "category": "electricity",
|
||||
"emission": round(grid_kwh * EMISSION_FACTOR, 2),
|
||||
"reduction": 0.0,
|
||||
"energy_consumption": round(grid_kwh, 2),
|
||||
"energy_unit": "kWh",
|
||||
"note": "园区用电碳排放",
|
||||
})
|
||||
|
||||
# PV generation reduction (Scope 2 avoided)
|
||||
pv_kwh = daily_pv_gen.get(date_str, 0)
|
||||
if pv_kwh > 0:
|
||||
carbon_rows.append({
|
||||
"date": dt, "scope": 2, "category": "pv_generation",
|
||||
"emission": 0.0,
|
||||
"reduction": round(pv_kwh * EMISSION_FACTOR, 2),
|
||||
"energy_consumption": round(pv_kwh, 2),
|
||||
"energy_unit": "kWh",
|
||||
"note": "光伏发电碳减排",
|
||||
})
|
||||
|
||||
# Heat pump saving (COP-based reduction vs electric heating)
|
||||
hp_kwh = daily_hp_consumption.get(date_str, 0)
|
||||
if hp_kwh > 0:
|
||||
avg_cop_day = 3.2
|
||||
heat_delivered = hp_kwh * avg_cop_day
|
||||
electric_heating_kwh = heat_delivered # COP=1 for electric heating
|
||||
saved_kwh = electric_heating_kwh - hp_kwh
|
||||
carbon_rows.append({
|
||||
"date": dt, "scope": 2, "category": "heat_pump_saving",
|
||||
"emission": 0.0,
|
||||
"reduction": round(saved_kwh * EMISSION_FACTOR, 2),
|
||||
"energy_consumption": round(saved_kwh, 2),
|
||||
"energy_unit": "kWh",
|
||||
"note": "热泵节能碳减排(相比电加热)",
|
||||
})
|
||||
|
||||
print(f" Generated {len(carbon_rows)} carbon emission rows")
|
||||
|
||||
# ---- Bulk insert ----
|
||||
BATCH = 2000
|
||||
|
||||
@@ -183,7 +352,8 @@ async def backfill():
|
||||
batch = energy_rows[i : i + BATCH]
|
||||
await session.execute(insert_energy, batch)
|
||||
done = min(i + BATCH, len(energy_rows))
|
||||
print(f" energy_data: {done}/{len(energy_rows)}")
|
||||
if done % 10000 < BATCH:
|
||||
print(f" energy_data: {done}/{len(energy_rows)}")
|
||||
await session.commit()
|
||||
print(" energy_data done.")
|
||||
|
||||
@@ -192,21 +362,37 @@ async def backfill():
|
||||
insert_summary = text("""
|
||||
INSERT INTO energy_daily_summary
|
||||
(device_id, date, energy_type, total_consumption, total_generation,
|
||||
peak_power, min_power, avg_power, operating_hours, cost, carbon_emission)
|
||||
peak_power, min_power, avg_power, operating_hours, avg_cop, cost, carbon_emission)
|
||||
VALUES
|
||||
(:device_id, :date, :energy_type, :total_consumption, :total_generation,
|
||||
:peak_power, :min_power, :avg_power, :operating_hours, :cost, :carbon_emission)
|
||||
:peak_power, :min_power, :avg_power, :operating_hours, :avg_cop, :cost, :carbon_emission)
|
||||
""")
|
||||
for i in range(0, len(summary_rows), BATCH):
|
||||
batch = summary_rows[i : i + BATCH]
|
||||
await session.execute(insert_summary, batch)
|
||||
done = min(i + BATCH, len(summary_rows))
|
||||
print(f" daily_summary: {done}/{len(summary_rows)}")
|
||||
await session.commit()
|
||||
print(" daily_summary done.")
|
||||
print(f" daily_summary done. ({len(summary_rows)} rows)")
|
||||
|
||||
# Insert carbon emissions
|
||||
print("Inserting carbon_emissions ...")
|
||||
insert_carbon = text("""
|
||||
INSERT INTO carbon_emissions
|
||||
(date, scope, category, emission, reduction,
|
||||
energy_consumption, energy_unit, note)
|
||||
VALUES
|
||||
(:date, :scope, :category, :emission, :reduction,
|
||||
:energy_consumption, :energy_unit, :note)
|
||||
""")
|
||||
for i in range(0, len(carbon_rows), BATCH):
|
||||
batch = carbon_rows[i : i + BATCH]
|
||||
await session.execute(insert_carbon, batch)
|
||||
await session.commit()
|
||||
print(f" carbon_emissions done. ({len(carbon_rows)} rows)")
|
||||
|
||||
await engine.dispose()
|
||||
print("=" * 60)
|
||||
print("Backfill complete!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
269
scripts/gitea_setup_team.sh
Normal file
269
scripts/gitea_setup_team.sh
Normal file
@@ -0,0 +1,269 @@
|
||||
#!/bin/bash
|
||||
# ============================================================
|
||||
# Gitea Organization & Team Setup Script
|
||||
# Run this after Gitea is back online (currently 502)
|
||||
#
|
||||
# Usage:
|
||||
# 1. Edit the USERS array below with your colleagues' info
|
||||
# 2. Generate an admin API token from Gitea Web UI:
|
||||
# -> Profile -> Settings -> Applications -> Generate Token
|
||||
# 3. Run: bash scripts/gitea_setup_team.sh
|
||||
# ============================================================
|
||||
|
||||
GITEA_URL="http://100.108.180.60:3300"
|
||||
ADMIN_TOKEN="" # <-- Paste your admin token here
|
||||
|
||||
ORG_NAME="tianpu"
|
||||
ORG_DISPLAY="天普零碳园区"
|
||||
REPO_NAME="tianpu-ems"
|
||||
|
||||
# ============================================================
|
||||
# Define your colleagues here
|
||||
# Format: "username:email:fullname:team"
|
||||
# team = developers | readonly
|
||||
# ============================================================
|
||||
USERS=(
|
||||
"zhangsan:zhangsan@example.com:张三:developers"
|
||||
"lisi:lisi@example.com:李四:developers"
|
||||
"wangwu:wangwu@example.com:王五:developers"
|
||||
"zhaoliu:zhaoliu@example.com:赵六:readonly"
|
||||
# Add more as needed...
|
||||
)
|
||||
|
||||
DEFAULT_PASSWORD="Tianpu@2026" # Users must change on first login
|
||||
|
||||
# ============================================================
|
||||
# Color output helpers
|
||||
# ============================================================
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
API="$GITEA_URL/api/v1"
|
||||
AUTH="Authorization: token $ADMIN_TOKEN"
|
||||
|
||||
# ============================================================
|
||||
# Pre-flight check
|
||||
# ============================================================
|
||||
if [ -z "$ADMIN_TOKEN" ]; then
|
||||
error "Please set ADMIN_TOKEN first!"
|
||||
echo ""
|
||||
echo " Steps to get a token:"
|
||||
echo " 1. Open $GITEA_URL in your browser"
|
||||
echo " 2. Login as admin (tianpu)"
|
||||
echo " 3. Go to: Settings -> Applications -> Generate New Token"
|
||||
echo " 4. Name: 'admin-setup', Scopes: check ALL"
|
||||
echo " 5. Copy the token and paste it into this script"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "============================================"
|
||||
echo " Gitea Team Setup - $ORG_DISPLAY"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
|
||||
# Check Gitea is alive
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 "$GITEA_URL")
|
||||
if [ "$HTTP_CODE" != "200" ]; then
|
||||
error "Gitea returned HTTP $HTTP_CODE. Is it running?"
|
||||
exit 1
|
||||
fi
|
||||
info "Gitea is online"
|
||||
|
||||
# ============================================================
|
||||
# Step 1: Create Organization (idempotent)
|
||||
# ============================================================
|
||||
echo ""
|
||||
echo "--- Step 1: Organization ---"
|
||||
|
||||
ORG_CHECK=$(curl -s -w "\n%{http_code}" -H "$AUTH" "$API/orgs/$ORG_NAME")
|
||||
ORG_HTTP=$(echo "$ORG_CHECK" | tail -1)
|
||||
|
||||
if [ "$ORG_HTTP" = "200" ]; then
|
||||
info "Organization '$ORG_NAME' already exists"
|
||||
else
|
||||
RESULT=$(curl -s -w "\n%{http_code}" -X POST "$API/orgs" \
|
||||
-H "$AUTH" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"username\": \"$ORG_NAME\",
|
||||
\"full_name\": \"$ORG_DISPLAY\",
|
||||
\"description\": \"天普零碳园区智慧能源管理\",
|
||||
\"visibility\": \"private\"
|
||||
}")
|
||||
HTTP=$(echo "$RESULT" | tail -1)
|
||||
if [ "$HTTP" = "201" ]; then
|
||||
info "Created organization '$ORG_NAME'"
|
||||
else
|
||||
error "Failed to create org (HTTP $HTTP)"
|
||||
echo "$RESULT" | head -1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# Step 2: Create Teams
|
||||
# ============================================================
|
||||
echo ""
|
||||
echo "--- Step 2: Teams ---"
|
||||
|
||||
create_team() {
|
||||
local TEAM_NAME=$1
|
||||
local PERMISSION=$2
|
||||
local DESCRIPTION=$3
|
||||
|
||||
TEAM_CHECK=$(curl -s -w "\n%{http_code}" -H "$AUTH" "$API/orgs/$ORG_NAME/teams")
|
||||
EXISTING=$(echo "$TEAM_CHECK" | head -1 | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
teams = json.load(sys.stdin)
|
||||
for t in teams:
|
||||
if t['name'] == '$TEAM_NAME':
|
||||
print(t['id'])
|
||||
break
|
||||
except: pass
|
||||
" 2>/dev/null)
|
||||
|
||||
if [ -n "$EXISTING" ]; then
|
||||
info "Team '$TEAM_NAME' already exists (id=$EXISTING)"
|
||||
echo "$EXISTING"
|
||||
return
|
||||
fi
|
||||
|
||||
RESULT=$(curl -s -w "\n%{http_code}" -X POST "$API/orgs/$ORG_NAME/teams" \
|
||||
-H "$AUTH" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"name\": \"$TEAM_NAME\",
|
||||
\"description\": \"$DESCRIPTION\",
|
||||
\"permission\": \"$PERMISSION\",
|
||||
\"includes_all_repositories\": true,
|
||||
\"units\": [\"repo.code\", \"repo.issues\", \"repo.pulls\", \"repo.releases\", \"repo.wiki\"]
|
||||
}")
|
||||
HTTP=$(echo "$RESULT" | tail -1)
|
||||
TEAM_ID=$(echo "$RESULT" | head -1 | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null)
|
||||
|
||||
if [ "$HTTP" = "201" ]; then
|
||||
info "Created team '$TEAM_NAME' (id=$TEAM_ID)"
|
||||
echo "$TEAM_ID"
|
||||
else
|
||||
error "Failed to create team '$TEAM_NAME' (HTTP $HTTP)"
|
||||
echo "$RESULT" | head -1
|
||||
fi
|
||||
}
|
||||
|
||||
DEV_TEAM_ID=$(create_team "developers" "write" "开发团队 - 可读写代码、提PR、管理Issues")
|
||||
RO_TEAM_ID=$(create_team "readonly" "read" "只读团队 - 仅可查看代码和Issues")
|
||||
|
||||
# ============================================================
|
||||
# Step 3: Fork/transfer repo to org (or add repo to teams)
|
||||
# ============================================================
|
||||
echo ""
|
||||
echo "--- Step 3: Repository ---"
|
||||
|
||||
# Check if repo already exists under org
|
||||
REPO_CHECK=$(curl -s -w "\n%{http_code}" -H "$AUTH" "$API/repos/$ORG_NAME/$REPO_NAME")
|
||||
REPO_HTTP=$(echo "$REPO_CHECK" | tail -1)
|
||||
|
||||
if [ "$REPO_HTTP" = "200" ]; then
|
||||
info "Repo '$ORG_NAME/$REPO_NAME' already exists under organization"
|
||||
else
|
||||
warn "Repo not found under org. You have two options:"
|
||||
echo ""
|
||||
echo " Option A: Transfer existing repo to org (recommended)"
|
||||
echo " Go to: $GITEA_URL/tianpu/$REPO_NAME/settings"
|
||||
echo " -> Danger Zone -> Transfer Repository -> new owner: $ORG_NAME"
|
||||
echo ""
|
||||
echo " Option B: Change remote URL after transfer:"
|
||||
echo " git remote set-url origin $GITEA_URL/$ORG_NAME/$REPO_NAME.git"
|
||||
echo ""
|
||||
echo " After transfer, re-run this script to add repo to teams."
|
||||
fi
|
||||
|
||||
# Add repo to teams (if repo exists under org)
|
||||
if [ "$REPO_HTTP" = "200" ]; then
|
||||
for TEAM_ID in $DEV_TEAM_ID $RO_TEAM_ID; do
|
||||
if [ -n "$TEAM_ID" ] && [[ "$TEAM_ID" =~ ^[0-9]+$ ]]; then
|
||||
curl -s -X PUT "$API/teams/$TEAM_ID/repos/$ORG_NAME/$REPO_NAME" \
|
||||
-H "$AUTH" > /dev/null 2>&1
|
||||
info "Added repo to team id=$TEAM_ID"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# Step 4: Create Users & Add to Teams
|
||||
# ============================================================
|
||||
echo ""
|
||||
echo "--- Step 4: Users ---"
|
||||
|
||||
for USER_ENTRY in "${USERS[@]}"; do
|
||||
IFS=':' read -r USERNAME EMAIL FULLNAME TEAM <<< "$USER_ENTRY"
|
||||
|
||||
# Create user
|
||||
USER_CHECK=$(curl -s -w "\n%{http_code}" -H "$AUTH" "$API/users/$USERNAME")
|
||||
USER_HTTP=$(echo "$USER_CHECK" | tail -1)
|
||||
|
||||
if [ "$USER_HTTP" = "200" ]; then
|
||||
info "User '$USERNAME' already exists"
|
||||
else
|
||||
RESULT=$(curl -s -w "\n%{http_code}" -X POST "$API/admin/users" \
|
||||
-H "$AUTH" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"username\": \"$USERNAME\",
|
||||
\"email\": \"$EMAIL\",
|
||||
\"full_name\": \"$FULLNAME\",
|
||||
\"password\": \"$DEFAULT_PASSWORD\",
|
||||
\"must_change_password\": true,
|
||||
\"visibility\": \"private\"
|
||||
}")
|
||||
HTTP=$(echo "$RESULT" | tail -1)
|
||||
if [ "$HTTP" = "201" ]; then
|
||||
info "Created user '$USERNAME' ($FULLNAME)"
|
||||
else
|
||||
error "Failed to create user '$USERNAME' (HTTP $HTTP)"
|
||||
echo "$RESULT" | head -1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Add to team
|
||||
if [ "$TEAM" = "developers" ] && [ -n "$DEV_TEAM_ID" ] && [[ "$DEV_TEAM_ID" =~ ^[0-9]+$ ]]; then
|
||||
curl -s -X PUT "$API/teams/$DEV_TEAM_ID/members/$USERNAME" -H "$AUTH" > /dev/null 2>&1
|
||||
info " -> Added '$USERNAME' to 'developers' team"
|
||||
elif [ "$TEAM" = "readonly" ] && [ -n "$RO_TEAM_ID" ] && [[ "$RO_TEAM_ID" =~ ^[0-9]+$ ]]; then
|
||||
curl -s -X PUT "$API/teams/$RO_TEAM_ID/members/$USERNAME" -H "$AUTH" > /dev/null 2>&1
|
||||
info " -> Added '$USERNAME' to 'readonly' team"
|
||||
fi
|
||||
done
|
||||
|
||||
# ============================================================
|
||||
# Summary
|
||||
# ============================================================
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Setup Complete!"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo " Organization: $GITEA_URL/$ORG_NAME"
|
||||
echo " Repository: $GITEA_URL/$ORG_NAME/$REPO_NAME"
|
||||
echo ""
|
||||
echo " Teams:"
|
||||
echo " developers (write) - can push, create PRs, manage issues"
|
||||
echo " readonly (read) - can view code and issues only"
|
||||
echo ""
|
||||
echo " Default password: $DEFAULT_PASSWORD"
|
||||
echo " (Users must change on first login)"
|
||||
echo ""
|
||||
echo " Each colleague clones with:"
|
||||
echo " git clone $GITEA_URL/$ORG_NAME/$REPO_NAME.git"
|
||||
echo ""
|
||||
echo " Recommended workflow:"
|
||||
echo " 1. Each dev creates a feature branch"
|
||||
echo " 2. Push branch, create Pull Request"
|
||||
echo " 3. Code review, then merge to main"
|
||||
echo "============================================"
|
||||
145
scripts/quick-start.sh
Normal file
145
scripts/quick-start.sh
Normal file
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# 天普零碳园区智慧能源管理平台 - 快速启动脚本
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# 检查前置依赖
|
||||
check_prerequisites() {
|
||||
log_info "检查前置依赖..."
|
||||
|
||||
if ! command -v docker &> /dev/null; then
|
||||
log_error "未找到 Docker,请先安装 Docker: https://docs.docker.com/get-docker/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker compose version &> /dev/null && ! docker-compose version &> /dev/null; then
|
||||
log_error "未找到 Docker Compose,请先安装 Docker Compose"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "前置依赖检查通过"
|
||||
}
|
||||
|
||||
# 确定 docker compose 命令
|
||||
get_compose_cmd() {
|
||||
if docker compose version &> /dev/null; then
|
||||
echo "docker compose"
|
||||
else
|
||||
echo "docker-compose"
|
||||
fi
|
||||
}
|
||||
|
||||
# 初始化环境变量
|
||||
init_env() {
|
||||
if [ ! -f .env ]; then
|
||||
log_warn ".env 文件不存在,从模板创建..."
|
||||
if [ -f .env.example ]; then
|
||||
cp .env.example .env
|
||||
log_warn "已创建 .env 文件,请根据实际情况修改配置"
|
||||
log_warn "当前使用默认配置启动,生产环境请务必修改密码和密钥"
|
||||
else
|
||||
log_error "未找到 .env.example 模板文件"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log_info ".env 文件已存在"
|
||||
fi
|
||||
}
|
||||
|
||||
# 启动服务
|
||||
start_services() {
|
||||
local compose_cmd
|
||||
compose_cmd=$(get_compose_cmd)
|
||||
|
||||
log_info "启动服务..."
|
||||
$compose_cmd up -d
|
||||
|
||||
log_info "等待服务就绪..."
|
||||
|
||||
# 等待数据库就绪
|
||||
local retries=30
|
||||
while [ $retries -gt 0 ]; do
|
||||
if docker exec tianpu_db pg_isready -U tianpu -d tianpu_ems &> /dev/null; then
|
||||
log_info "数据库已就绪"
|
||||
break
|
||||
fi
|
||||
retries=$((retries - 1))
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [ $retries -eq 0 ]; then
|
||||
log_error "数据库启动超时"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 等待后端就绪
|
||||
retries=30
|
||||
while [ $retries -gt 0 ]; do
|
||||
if docker exec tianpu_backend curl -sf http://localhost:8000/health &> /dev/null; then
|
||||
log_info "后端服务已就绪"
|
||||
break
|
||||
fi
|
||||
retries=$((retries - 1))
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [ $retries -eq 0 ]; then
|
||||
log_error "后端服务启动超时"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 初始化数据
|
||||
init_data() {
|
||||
log_info "初始化数据库..."
|
||||
docker exec tianpu_backend python scripts/init_db.py || {
|
||||
log_warn "数据库初始化跳过(可能已初始化)"
|
||||
}
|
||||
|
||||
log_info "写入种子数据..."
|
||||
docker exec tianpu_backend python scripts/seed_data.py || {
|
||||
log_warn "种子数据写入跳过(可能已存在)"
|
||||
}
|
||||
}
|
||||
|
||||
# 打印访问信息
|
||||
print_info() {
|
||||
echo ""
|
||||
echo "============================================="
|
||||
echo " 天普零碳园区智慧能源管理平台 启动完成"
|
||||
echo "============================================="
|
||||
echo ""
|
||||
echo " 前端页面: http://localhost:3000"
|
||||
echo " 后端 API: http://localhost:8000"
|
||||
echo " API 文档: http://localhost:8000/docs"
|
||||
echo ""
|
||||
echo " 默认账号: admin"
|
||||
echo " 默认密码: admin123"
|
||||
echo ""
|
||||
echo " 请在首次登录后修改默认密码"
|
||||
echo "============================================="
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 主流程
|
||||
main() {
|
||||
log_info "天普零碳园区智慧能源管理平台 - 快速启动"
|
||||
echo ""
|
||||
|
||||
check_prerequisites
|
||||
init_env
|
||||
start_services
|
||||
init_data
|
||||
print_info
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,191 +1,553 @@
|
||||
"""种子数据 - 天普园区设备信息和初始用户"""
|
||||
"""种子数据 - 天普园区设备信息、用户、告警规则、碳排放因子、报表模板、历史告警"""
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
sys.path.insert(0, "../backend")
|
||||
import os
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
from app.core.database import async_session
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "backend"))
|
||||
|
||||
from sqlalchemy import select
|
||||
from app.core.database import async_session, engine
|
||||
from app.core.security import hash_password
|
||||
from app.models.user import User, Role
|
||||
from app.models.device import Device, DeviceType, DeviceGroup
|
||||
from app.models.alarm import AlarmRule, AlarmEvent
|
||||
from app.models.carbon import EmissionFactor
|
||||
from app.models.report import ReportTemplate
|
||||
|
||||
|
||||
async def seed():
|
||||
async with async_session() as session:
|
||||
# 1. 角色
|
||||
roles = [
|
||||
Role(name="admin", display_name="园区管理员", description="平台最高管理者,负责全局配置和用户管理"),
|
||||
Role(name="energy_manager", display_name="能源主管", description="负责园区能源运行管理和优化决策"),
|
||||
Role(name="area_manager", display_name="区域负责人", description="负责特定区域或建筑的能源管理"),
|
||||
Role(name="operator", display_name="设备运维员", description="负责设备日常运维和故障处理"),
|
||||
Role(name="analyst", display_name="财务分析员", description="负责能源成本分析和财务报表"),
|
||||
Role(name="visitor", display_name="普通访客", description="仅查看公开信息"),
|
||||
# =====================================================================
|
||||
# 1. 角色 (6 roles with JSON permissions)
|
||||
# =====================================================================
|
||||
roles_data = [
|
||||
{
|
||||
"name": "admin",
|
||||
"display_name": "园区管理员",
|
||||
"description": "平台最高管理者,负责全局配置和用户管理",
|
||||
"permissions": json.dumps([
|
||||
"user:read", "user:write", "user:delete",
|
||||
"device:read", "device:write", "device:delete",
|
||||
"energy:read", "energy:export",
|
||||
"alarm:read", "alarm:write", "alarm:acknowledge",
|
||||
"report:read", "report:write", "report:export",
|
||||
"carbon:read", "carbon:write",
|
||||
"system:config", "system:audit",
|
||||
]),
|
||||
},
|
||||
{
|
||||
"name": "energy_manager",
|
||||
"display_name": "能源主管",
|
||||
"description": "负责园区能源运行管理和优化决策",
|
||||
"permissions": json.dumps([
|
||||
"device:read", "device:write",
|
||||
"energy:read", "energy:export",
|
||||
"alarm:read", "alarm:write", "alarm:acknowledge",
|
||||
"report:read", "report:write", "report:export",
|
||||
"carbon:read", "carbon:write",
|
||||
]),
|
||||
},
|
||||
{
|
||||
"name": "area_manager",
|
||||
"display_name": "区域负责人",
|
||||
"description": "负责特定区域或建筑的能源管理",
|
||||
"permissions": json.dumps([
|
||||
"device:read",
|
||||
"energy:read", "energy:export",
|
||||
"alarm:read", "alarm:acknowledge",
|
||||
"report:read", "report:export",
|
||||
"carbon:read",
|
||||
]),
|
||||
},
|
||||
{
|
||||
"name": "operator",
|
||||
"display_name": "设备运维员",
|
||||
"description": "负责设备日常运维和故障处理",
|
||||
"permissions": json.dumps([
|
||||
"device:read", "device:write",
|
||||
"energy:read",
|
||||
"alarm:read", "alarm:acknowledge",
|
||||
"report:read",
|
||||
]),
|
||||
},
|
||||
{
|
||||
"name": "analyst",
|
||||
"display_name": "财务分析员",
|
||||
"description": "负责能源成本分析和财务报表",
|
||||
"permissions": json.dumps([
|
||||
"energy:read", "energy:export",
|
||||
"report:read", "report:write", "report:export",
|
||||
"carbon:read",
|
||||
]),
|
||||
},
|
||||
{
|
||||
"name": "visitor",
|
||||
"display_name": "普通访客",
|
||||
"description": "仅查看公开信息",
|
||||
"permissions": json.dumps([
|
||||
"energy:read",
|
||||
"device:read",
|
||||
]),
|
||||
},
|
||||
]
|
||||
roles = [Role(**r) for r in roles_data]
|
||||
session.add_all(roles)
|
||||
|
||||
# 2. 默认用户
|
||||
# =====================================================================
|
||||
# 2. 用户 (admin + 2 demo users)
|
||||
# =====================================================================
|
||||
users = [
|
||||
User(username="admin", hashed_password=hash_password("admin123"), full_name="系统管理员", role="admin", email="admin@tianpu.com"),
|
||||
User(username="energy_mgr", hashed_password=hash_password("tianpu123"), full_name="能源主管", role="energy_manager", email="energy@tianpu.com"),
|
||||
User(username="operator1", hashed_password=hash_password("tianpu123"), full_name="运维工程师", role="operator", email="op1@tianpu.com"),
|
||||
User(username="admin", hashed_password=hash_password("admin123"),
|
||||
full_name="系统管理员", role="admin", email="admin@tianpu.com",
|
||||
phone="13800000001"),
|
||||
User(username="energy_mgr", hashed_password=hash_password("tianpu123"),
|
||||
full_name="张能源", role="energy_manager", email="energy@tianpu.com",
|
||||
phone="13800000002"),
|
||||
User(username="operator1", hashed_password=hash_password("tianpu123"),
|
||||
full_name="李运维", role="operator", email="op1@tianpu.com",
|
||||
phone="13800000003"),
|
||||
]
|
||||
session.add_all(users)
|
||||
|
||||
# 3. 设备类型
|
||||
# =====================================================================
|
||||
# 3. 设备类型 (8 types)
|
||||
# =====================================================================
|
||||
device_types = [
|
||||
DeviceType(code="pv_inverter", name="光伏逆变器", icon="solar-panel",
|
||||
data_fields=["power", "daily_energy", "total_energy", "dc_voltage", "ac_voltage", "temperature", "fault_code"]),
|
||||
data_fields=["power", "daily_energy", "total_energy",
|
||||
"dc_voltage", "ac_voltage", "temperature", "fault_code"]),
|
||||
DeviceType(code="heat_pump", name="空气源热泵", icon="heat-pump",
|
||||
data_fields=["power", "cop", "inlet_temp", "outlet_temp", "flow_rate", "outdoor_temp", "mode"]),
|
||||
data_fields=["power", "cop", "inlet_temp", "outlet_temp",
|
||||
"flow_rate", "outdoor_temp", "mode"]),
|
||||
DeviceType(code="solar_thermal", name="光热集热器", icon="solar-thermal",
|
||||
data_fields=["heat_output", "collector_temp", "irradiance", "pump_status"]),
|
||||
DeviceType(code="battery", name="储能系统", icon="battery",
|
||||
data_fields=["power", "soc", "voltage", "current", "temperature", "cycle_count"]),
|
||||
data_fields=["power", "soc", "voltage", "current",
|
||||
"temperature", "cycle_count"]),
|
||||
DeviceType(code="meter", name="智能电表", icon="meter",
|
||||
data_fields=["power", "energy", "voltage", "current", "power_factor", "frequency"]),
|
||||
data_fields=["power", "energy", "voltage", "current",
|
||||
"power_factor", "frequency"]),
|
||||
DeviceType(code="sensor", name="温湿度传感器", icon="sensor",
|
||||
data_fields=["temperature", "humidity"]),
|
||||
DeviceType(code="heat_meter", name="热量表", icon="heat-meter",
|
||||
data_fields=["heat_power", "heat_energy", "flow_rate", "supply_temp", "return_temp"]),
|
||||
data_fields=["heat_power", "heat_energy", "flow_rate",
|
||||
"supply_temp", "return_temp"]),
|
||||
DeviceType(code="water_meter", name="水表", icon="water-meter",
|
||||
data_fields=["flow_rate", "total_flow"]),
|
||||
]
|
||||
session.add_all(device_types)
|
||||
|
||||
# 4. 设备分组
|
||||
# =====================================================================
|
||||
# 4. 设备分组 (hierarchical: Campus -> subsystems)
|
||||
# =====================================================================
|
||||
groups = [
|
||||
DeviceGroup(id=1, name="光伏系统", location="天普大楼屋顶"),
|
||||
DeviceGroup(id=2, name="热泵系统", location="天普大楼机房"),
|
||||
DeviceGroup(id=3, name="电力计量", location="天普大楼配电室"),
|
||||
DeviceGroup(id=4, name="环境监测", location="天普大楼各楼层"),
|
||||
DeviceGroup(id=1, name="天普大兴园区", location="北京市大兴区", description="园区总节点"),
|
||||
DeviceGroup(id=2, name="东楼", parent_id=1, location="天普大楼东侧"),
|
||||
DeviceGroup(id=3, name="西楼", parent_id=1, location="天普大楼西侧"),
|
||||
DeviceGroup(id=4, name="光伏系统", parent_id=1, location="天普大楼屋顶"),
|
||||
DeviceGroup(id=5, name="热泵系统", parent_id=1, location="天普大楼机房"),
|
||||
DeviceGroup(id=6, name="电力计量", parent_id=1, location="天普大楼配电室"),
|
||||
DeviceGroup(id=7, name="环境监测", parent_id=1, location="天普大楼各楼层"),
|
||||
]
|
||||
session.add_all(groups)
|
||||
|
||||
# Flush to satisfy foreign key constraints before inserting devices
|
||||
# Flush to satisfy FK constraints before inserting devices
|
||||
await session.flush()
|
||||
|
||||
# 5. 天普实际设备
|
||||
# =====================================================================
|
||||
# 5. 天普实际设备 (19 devices total)
|
||||
# =====================================================================
|
||||
devices = [
|
||||
# 光伏逆变器 - 3台华为SUN2000-110KTL-M0
|
||||
Device(name="东楼逆变器1", code="INV-01", device_type="pv_inverter", group_id=1,
|
||||
# --- 光伏逆变器 - 3台华为SUN2000-110KTL-M0 ---
|
||||
Device(name="东楼逆变器1", code="INV-01", device_type="pv_inverter", group_id=4,
|
||||
model="SUN2000-110KTL-M0", manufacturer="华为", rated_power=110,
|
||||
location="东楼屋顶", protocol="http_api", collect_interval=15,
|
||||
connection_params={"api_type": "fusionsolar", "station_code": "NE=12345"}),
|
||||
Device(name="东楼逆变器2", code="INV-02", device_type="pv_inverter", group_id=1,
|
||||
Device(name="东楼逆变器2", code="INV-02", device_type="pv_inverter", group_id=4,
|
||||
model="SUN2000-110KTL-M0", manufacturer="华为", rated_power=110,
|
||||
location="东楼屋顶", protocol="http_api", collect_interval=15),
|
||||
Device(name="西楼逆变器1", code="INV-03", device_type="pv_inverter", group_id=1,
|
||||
Device(name="西楼逆变器1", code="INV-03", device_type="pv_inverter", group_id=4,
|
||||
model="SUN2000-110KTL-M0", manufacturer="华为", rated_power=110,
|
||||
location="西楼屋顶", protocol="http_api", collect_interval=15),
|
||||
|
||||
# 热泵机组 - 4台
|
||||
Device(name="热泵机组1", code="HP-01", device_type="heat_pump", group_id=2,
|
||||
# --- 热泵机组 - 4台 ---
|
||||
Device(name="热泵机组1", code="HP-01", device_type="heat_pump", group_id=5,
|
||||
rated_power=35, location="大楼机房", protocol="modbus_rtu", collect_interval=15,
|
||||
connection_params={"dtu_id": "2225000009", "slave_id": 1}),
|
||||
Device(name="热泵机组2", code="HP-02", device_type="heat_pump", group_id=2,
|
||||
Device(name="热泵机组2", code="HP-02", device_type="heat_pump", group_id=5,
|
||||
rated_power=35, location="大楼机房", protocol="modbus_rtu", collect_interval=15,
|
||||
connection_params={"dtu_id": "2225000009", "slave_id": 2}),
|
||||
Device(name="热泵机组3", code="HP-03", device_type="heat_pump", group_id=2,
|
||||
Device(name="热泵机组3", code="HP-03", device_type="heat_pump", group_id=5,
|
||||
rated_power=35, location="大楼机房", protocol="modbus_rtu", collect_interval=15,
|
||||
connection_params={"dtu_id": "2225000009", "slave_id": 3}),
|
||||
Device(name="热泵机组4", code="HP-04", device_type="heat_pump", group_id=2,
|
||||
Device(name="热泵机组4", code="HP-04", device_type="heat_pump", group_id=5,
|
||||
rated_power=35, location="大楼机房", protocol="modbus_rtu", collect_interval=15,
|
||||
connection_params={"dtu_id": "2225000009", "slave_id": 4}),
|
||||
|
||||
# 电表
|
||||
Device(name="关口电表(余电上网)", code="METER-GRID", device_type="meter", group_id=3,
|
||||
model="威胜", serial_number="3462847657", location="配电室", protocol="dlt645", collect_interval=60,
|
||||
# --- 电表 ---
|
||||
Device(name="关口电表(余电上网)", code="METER-GRID", device_type="meter", group_id=6,
|
||||
model="威胜", serial_number="3462847657", location="配电室",
|
||||
protocol="dlt645", collect_interval=60,
|
||||
connection_params={"dtu_id": "infrared", "ratio": 1000}),
|
||||
Device(name="并网电表(光伏总发电)", code="METER-PV", device_type="meter", group_id=3,
|
||||
model="杭州炬华", serial_number="3422994056", location="配电室", protocol="dlt645", collect_interval=60,
|
||||
Device(name="并网电表(光伏总发电)", code="METER-PV", device_type="meter", group_id=6,
|
||||
model="杭州炬华", serial_number="3422994056", location="配电室",
|
||||
protocol="dlt645", collect_interval=60,
|
||||
connection_params={"dtu_id": "infrared", "ct_ratio": "600/5"}),
|
||||
Device(name="热泵电表", code="METER-HP", device_type="meter", group_id=3,
|
||||
Device(name="热泵电表", code="METER-HP", device_type="meter", group_id=6,
|
||||
location="机房热泵控制柜", protocol="modbus_rtu", collect_interval=60,
|
||||
connection_params={"dtu_id": "2225000003"}),
|
||||
Device(name="循环水泵电表", code="METER-PUMP", device_type="meter", group_id=3,
|
||||
Device(name="循环水泵电表", code="METER-PUMP", device_type="meter", group_id=6,
|
||||
location="机房水泵配电柜", protocol="modbus_rtu", collect_interval=60,
|
||||
connection_params={"dtu_id": "2225000002"}),
|
||||
|
||||
# 热量表
|
||||
Device(name="主管热量表", code="HM-01", device_type="heat_meter", group_id=2,
|
||||
# --- 热量表 ---
|
||||
Device(name="主管热量表", code="HM-01", device_type="heat_meter", group_id=5,
|
||||
location="机房中部主管", protocol="modbus_rtu", collect_interval=60,
|
||||
connection_params={"dtu_id": "2225000001"}),
|
||||
|
||||
# 温湿度传感器
|
||||
Device(name="一楼东厅温湿度", code="TH-01", device_type="sensor", group_id=4,
|
||||
# --- 温湿度传感器 ---
|
||||
Device(name="一楼东厅温湿度", code="TH-01", device_type="sensor", group_id=7,
|
||||
location="大楼一楼东厅", protocol="mqtt", collect_interval=60,
|
||||
connection_params={"dtu_id": "2225000007"}, metadata_={"area": "一楼东展厅风管上"}),
|
||||
Device(name="一楼西厅温湿度", code="TH-02", device_type="sensor", group_id=4,
|
||||
connection_params={"dtu_id": "2225000007"},
|
||||
metadata_={"area": "一楼东展厅风管上"}),
|
||||
Device(name="一楼西厅温湿度", code="TH-02", device_type="sensor", group_id=7,
|
||||
location="大楼一楼西厅", protocol="mqtt", collect_interval=60,
|
||||
connection_params={"dtu_id": "2225000006"}, metadata_={"area": "一楼西厅中西风管上"}),
|
||||
Device(name="二楼西厅温湿度", code="TH-03", device_type="sensor", group_id=4,
|
||||
connection_params={"dtu_id": "2225000006"},
|
||||
metadata_={"area": "一楼西厅中西风管上"}),
|
||||
Device(name="二楼西厅温湿度", code="TH-03", device_type="sensor", group_id=7,
|
||||
location="大楼二楼西厅", protocol="mqtt", collect_interval=60,
|
||||
connection_params={"dtu_id": "2225000005"}, metadata_={"area": "财务门口西侧"}),
|
||||
Device(name="二楼东厅温湿度", code="TH-04", device_type="sensor", group_id=4,
|
||||
connection_params={"dtu_id": "2225000005"},
|
||||
metadata_={"area": "财务门口西侧"}),
|
||||
Device(name="二楼东厅温湿度", code="TH-04", device_type="sensor", group_id=7,
|
||||
location="大楼二楼东厅", protocol="mqtt", collect_interval=60,
|
||||
connection_params={"dtu_id": "2225000004"}, metadata_={"area": "英豪对过"}),
|
||||
Device(name="机房室外温湿度", code="TH-05", device_type="sensor", group_id=4,
|
||||
connection_params={"dtu_id": "2225000004"},
|
||||
metadata_={"area": "英豪对过"}),
|
||||
Device(name="机房室外温湿度", code="TH-05", device_type="sensor", group_id=7,
|
||||
location="机房热泵控制柜", protocol="mqtt", collect_interval=60,
|
||||
connection_params={"dtu_id": "2225000008"}, metadata_={"area": "机房门口", "type": "outdoor"}),
|
||||
connection_params={"dtu_id": "2225000008"},
|
||||
metadata_={"area": "机房门口", "type": "outdoor"}),
|
||||
|
||||
# 水表
|
||||
Device(name="补水水表", code="WM-01", device_type="water_meter", group_id=2,
|
||||
# --- 水表 ---
|
||||
Device(name="补水水表", code="WM-01", device_type="water_meter", group_id=5,
|
||||
location="机房软水器补水管", protocol="image", collect_interval=300,
|
||||
connection_params={"type": "smart_capture"}),
|
||||
]
|
||||
session.add_all(devices)
|
||||
await session.flush()
|
||||
|
||||
# 6. 碳排放因子
|
||||
# =====================================================================
|
||||
# 6. 告警规则
|
||||
# =====================================================================
|
||||
alarm_rules = [
|
||||
AlarmRule(
|
||||
name="光伏功率过低告警",
|
||||
device_type="pv_inverter",
|
||||
data_type="power",
|
||||
condition="lt",
|
||||
threshold=5.0,
|
||||
duration=1800,
|
||||
severity="warning",
|
||||
notify_channels=["app", "wechat"],
|
||||
is_active=True,
|
||||
),
|
||||
AlarmRule(
|
||||
name="热泵COP过低告警",
|
||||
device_type="heat_pump",
|
||||
data_type="cop",
|
||||
condition="lt",
|
||||
threshold=2.0,
|
||||
duration=600,
|
||||
severity="major",
|
||||
notify_channels=["app", "sms", "wechat"],
|
||||
is_active=True,
|
||||
),
|
||||
AlarmRule(
|
||||
name="室内温度超限告警",
|
||||
device_type="sensor",
|
||||
data_type="temperature",
|
||||
condition="range_out",
|
||||
threshold_high=30.0,
|
||||
threshold_low=16.0,
|
||||
duration=300,
|
||||
severity="warning",
|
||||
notify_channels=["app"],
|
||||
is_active=True,
|
||||
),
|
||||
AlarmRule(
|
||||
name="电表通信中断告警",
|
||||
device_type="meter",
|
||||
data_type="power",
|
||||
condition="eq",
|
||||
threshold=0.0,
|
||||
duration=3600,
|
||||
severity="critical",
|
||||
notify_channels=["app", "sms", "wechat"],
|
||||
is_active=True,
|
||||
),
|
||||
AlarmRule(
|
||||
name="热泵功率过载告警",
|
||||
device_type="heat_pump",
|
||||
data_type="power",
|
||||
condition="gt",
|
||||
threshold=38.0,
|
||||
duration=300,
|
||||
severity="critical",
|
||||
notify_channels=["app", "sms"],
|
||||
is_active=True,
|
||||
),
|
||||
AlarmRule(
|
||||
name="光伏逆变器过温告警",
|
||||
device_type="pv_inverter",
|
||||
data_type="temperature",
|
||||
condition="gt",
|
||||
threshold=65.0,
|
||||
duration=120,
|
||||
severity="major",
|
||||
notify_channels=["app", "sms"],
|
||||
is_active=True,
|
||||
),
|
||||
]
|
||||
session.add_all(alarm_rules)
|
||||
|
||||
# =====================================================================
|
||||
# 7. 碳排放因子
|
||||
# =====================================================================
|
||||
factors = [
|
||||
EmissionFactor(name="华北电网排放因子", energy_type="electricity", factor=0.8843,
|
||||
unit="kWh", scope=2, region="north_china", source="生态环境部2023", year=2023),
|
||||
unit="kWh", scope=2, region="north_china",
|
||||
source="生态环境部2023", year=2023),
|
||||
EmissionFactor(name="天然气排放因子", energy_type="natural_gas", factor=2.162,
|
||||
unit="m³", scope=1, source="IPCC 2006", year=2006),
|
||||
EmissionFactor(name="柴油排放因子", energy_type="diesel", factor=2.63,
|
||||
unit="L", scope=1, source="IPCC 2006", year=2006),
|
||||
EmissionFactor(name="光伏减排因子", energy_type="pv_generation", factor=0.8843,
|
||||
unit="kWh", scope=2, region="north_china", source="等量替代电网电力", year=2023),
|
||||
unit="kWh", scope=2, region="north_china",
|
||||
source="等量替代电网电力", year=2023),
|
||||
EmissionFactor(name="热泵节能减排因子", energy_type="heat_pump_saving", factor=0.8843,
|
||||
unit="kWh", scope=2, region="north_china", source="相比电加热节省的电量", year=2023),
|
||||
unit="kWh", scope=2, region="north_china",
|
||||
source="相比电加热节省的电量", year=2023),
|
||||
]
|
||||
session.add_all(factors)
|
||||
|
||||
# 7. 预置报表模板
|
||||
# =====================================================================
|
||||
# 8. 预置报表模板
|
||||
# =====================================================================
|
||||
templates = [
|
||||
ReportTemplate(name="日报", report_type="daily", is_system=True,
|
||||
fields=[{"key": "total_consumption", "label": "总用电量", "unit": "kWh"},
|
||||
{"key": "pv_generation", "label": "光伏发电量", "unit": "kWh"},
|
||||
{"key": "self_use_rate", "label": "自消纳率", "unit": "%"},
|
||||
{"key": "heatpump_energy", "label": "热泵用电", "unit": "kWh"},
|
||||
{"key": "avg_cop", "label": "平均COP"},
|
||||
{"key": "carbon_emission", "label": "碳排放", "unit": "kgCO2"}]),
|
||||
ReportTemplate(name="月报", report_type="monthly", is_system=True,
|
||||
fields=[{"key": "total_consumption", "label": "总用电量", "unit": "kWh"},
|
||||
{"key": "pv_generation", "label": "光伏发电量", "unit": "kWh"},
|
||||
{"key": "grid_import", "label": "电网购电", "unit": "kWh"},
|
||||
{"key": "cost", "label": "电费", "unit": "元"},
|
||||
{"key": "carbon_emission", "label": "碳排放", "unit": "tCO2"},
|
||||
{"key": "carbon_reduction", "label": "碳减排", "unit": "tCO2"}],
|
||||
time_granularity="day"),
|
||||
ReportTemplate(name="设备运行报告", report_type="custom", is_system=True,
|
||||
fields=[{"key": "device_name", "label": "设备名称"},
|
||||
{"key": "operating_hours", "label": "运行时长", "unit": "h"},
|
||||
{"key": "energy_consumption", "label": "能耗", "unit": "kWh"},
|
||||
{"key": "avg_power", "label": "平均功率", "unit": "kW"},
|
||||
{"key": "alarm_count", "label": "告警次数"}]),
|
||||
ReportTemplate(
|
||||
name="日报", report_type="daily", is_system=True,
|
||||
description="每日能源运行日报,包含发用电量、自消纳率、热泵COP、碳排放",
|
||||
fields=[
|
||||
{"key": "total_consumption", "label": "总用电量", "unit": "kWh"},
|
||||
{"key": "pv_generation", "label": "光伏发电量", "unit": "kWh"},
|
||||
{"key": "self_use_rate", "label": "自消纳率", "unit": "%"},
|
||||
{"key": "heatpump_energy", "label": "热泵用电", "unit": "kWh"},
|
||||
{"key": "avg_cop", "label": "平均COP"},
|
||||
{"key": "carbon_emission", "label": "碳排放", "unit": "kgCO2"},
|
||||
],
|
||||
),
|
||||
ReportTemplate(
|
||||
name="月报", report_type="monthly", is_system=True,
|
||||
description="每月能源运行月报,包含能耗趋势、电费、碳排放与减排",
|
||||
fields=[
|
||||
{"key": "total_consumption", "label": "总用电量", "unit": "kWh"},
|
||||
{"key": "pv_generation", "label": "光伏发电量", "unit": "kWh"},
|
||||
{"key": "grid_import", "label": "电网购电", "unit": "kWh"},
|
||||
{"key": "cost", "label": "电费", "unit": "元"},
|
||||
{"key": "carbon_emission", "label": "碳排放", "unit": "tCO2"},
|
||||
{"key": "carbon_reduction", "label": "碳减排", "unit": "tCO2"},
|
||||
],
|
||||
time_granularity="day",
|
||||
),
|
||||
ReportTemplate(
|
||||
name="设备运行报告", report_type="custom", is_system=True,
|
||||
description="设备运行状态汇总,包含运行时长、能耗、告警统计",
|
||||
fields=[
|
||||
{"key": "device_name", "label": "设备名称"},
|
||||
{"key": "operating_hours", "label": "运行时长", "unit": "h"},
|
||||
{"key": "energy_consumption", "label": "能耗", "unit": "kWh"},
|
||||
{"key": "avg_power", "label": "平均功率", "unit": "kW"},
|
||||
{"key": "alarm_count", "label": "告警次数"},
|
||||
],
|
||||
),
|
||||
]
|
||||
session.add_all(templates)
|
||||
|
||||
await session.flush()
|
||||
|
||||
# =====================================================================
|
||||
# 9. 历史告警事件 (15 events over last 7 days)
|
||||
# =====================================================================
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# We need device IDs and rule IDs — query them back
|
||||
dev_result = await session.execute(select(Device))
|
||||
all_devices = {d.code: d.id for d in dev_result.scalars().all()}
|
||||
|
||||
rule_result = await session.execute(select(AlarmRule))
|
||||
all_rules = {r.name: r for r in rule_result.scalars().all()}
|
||||
|
||||
r_pv_low = all_rules["光伏功率过低告警"]
|
||||
r_hp_cop = all_rules["热泵COP过低告警"]
|
||||
r_temp = all_rules["室内温度超限告警"]
|
||||
r_meter = all_rules["电表通信中断告警"]
|
||||
r_hp_overload = all_rules["热泵功率过载告警"]
|
||||
r_pv_overtemp = all_rules["光伏逆变器过温告警"]
|
||||
|
||||
alarm_events = [
|
||||
# --- 7 days ago: PV low power (resolved) ---
|
||||
AlarmEvent(
|
||||
rule_id=r_pv_low.id, device_id=all_devices["INV-01"],
|
||||
severity="warning", title="光伏功率过低告警",
|
||||
description="当前值 2.3,阈值 5.0", value=2.3, threshold=5.0,
|
||||
status="resolved", resolve_note="云层过境后恢复正常",
|
||||
triggered_at=now - timedelta(days=7, hours=3),
|
||||
resolved_at=now - timedelta(days=7, hours=2, minutes=45),
|
||||
),
|
||||
# --- 6 days ago: Heat pump COP low (resolved) ---
|
||||
AlarmEvent(
|
||||
rule_id=r_hp_cop.id, device_id=all_devices["HP-01"],
|
||||
severity="major", title="热泵COP过低告警",
|
||||
description="当前值 1.6,阈值 2.0", value=1.6, threshold=2.0,
|
||||
status="resolved", resolve_note="设备恢复正常",
|
||||
triggered_at=now - timedelta(days=6, hours=10),
|
||||
resolved_at=now - timedelta(days=6, hours=9, minutes=30),
|
||||
),
|
||||
# --- 5 days ago: Temperature out of range (resolved) ---
|
||||
AlarmEvent(
|
||||
rule_id=r_temp.id, device_id=all_devices["TH-01"],
|
||||
severity="warning", title="室内温度超限告警",
|
||||
description="当前值 31.2,阈值 [16.0, 30.0]", value=31.2, threshold=30.0,
|
||||
status="resolved", resolve_note="已调节空调温度",
|
||||
triggered_at=now - timedelta(days=5, hours=14),
|
||||
resolved_at=now - timedelta(days=5, hours=13, minutes=20),
|
||||
),
|
||||
# --- 5 days ago: Meter communication lost (resolved) ---
|
||||
AlarmEvent(
|
||||
rule_id=r_meter.id, device_id=all_devices["METER-HP"],
|
||||
severity="critical", title="电表通信中断告警",
|
||||
description="当前值 0.0,阈值 0.0", value=0.0, threshold=0.0,
|
||||
status="resolved", resolve_note="已派人检修,通信恢复",
|
||||
triggered_at=now - timedelta(days=5, hours=8),
|
||||
resolved_at=now - timedelta(days=5, hours=6),
|
||||
),
|
||||
# --- 4 days ago: HP overload (resolved) ---
|
||||
AlarmEvent(
|
||||
rule_id=r_hp_overload.id, device_id=all_devices["HP-02"],
|
||||
severity="critical", title="热泵功率过载告警",
|
||||
description="当前值 40.2,阈值 38.0", value=40.2, threshold=38.0,
|
||||
status="resolved", resolve_note="负荷降低后自动恢复",
|
||||
triggered_at=now - timedelta(days=4, hours=16),
|
||||
resolved_at=now - timedelta(days=4, hours=15, minutes=40),
|
||||
),
|
||||
# --- 4 days ago: PV overtemp (resolved) ---
|
||||
AlarmEvent(
|
||||
rule_id=r_pv_overtemp.id, device_id=all_devices["INV-01"],
|
||||
severity="major", title="光伏逆变器过温告警",
|
||||
description="当前值 68.5,阈值 65.0", value=68.5, threshold=65.0,
|
||||
status="resolved", resolve_note="自动恢复",
|
||||
triggered_at=now - timedelta(days=4, hours=13),
|
||||
resolved_at=now - timedelta(days=4, hours=12, minutes=45),
|
||||
),
|
||||
# --- 3 days ago: PV low power again (resolved) ---
|
||||
AlarmEvent(
|
||||
rule_id=r_pv_low.id, device_id=all_devices["INV-03"],
|
||||
severity="warning", title="光伏功率过低告警",
|
||||
description="当前值 1.8,阈值 5.0", value=1.8, threshold=5.0,
|
||||
status="resolved", resolve_note="自动恢复",
|
||||
triggered_at=now - timedelta(days=3, hours=11),
|
||||
resolved_at=now - timedelta(days=3, hours=10, minutes=48),
|
||||
),
|
||||
# --- 2 days ago: Temperature sensor (acknowledged) ---
|
||||
AlarmEvent(
|
||||
rule_id=r_temp.id, device_id=all_devices["TH-03"],
|
||||
severity="warning", title="室内温度超限告警",
|
||||
description="当前值 15.2,阈值 [16.0, 30.0]", value=15.2, threshold=16.0,
|
||||
status="resolved", resolve_note="供暖开启后恢复",
|
||||
triggered_at=now - timedelta(days=2, hours=7),
|
||||
acknowledged_at=now - timedelta(days=2, hours=6, minutes=50),
|
||||
resolved_at=now - timedelta(days=2, hours=5),
|
||||
),
|
||||
# --- 2 days ago: HP COP low (acknowledged, then resolved) ---
|
||||
AlarmEvent(
|
||||
rule_id=r_hp_cop.id, device_id=all_devices["HP-03"],
|
||||
severity="major", title="热泵COP过低告警",
|
||||
description="当前值 1.4,阈值 2.0", value=1.4, threshold=2.0,
|
||||
status="resolved", resolve_note="已派人检修",
|
||||
triggered_at=now - timedelta(days=2, hours=15),
|
||||
acknowledged_at=now - timedelta(days=2, hours=14, minutes=30),
|
||||
resolved_at=now - timedelta(days=2, hours=12),
|
||||
),
|
||||
# --- 1 day ago: PV overtemp (resolved) ---
|
||||
AlarmEvent(
|
||||
rule_id=r_pv_overtemp.id, device_id=all_devices["INV-02"],
|
||||
severity="major", title="光伏逆变器过温告警",
|
||||
description="当前值 67.1,阈值 65.0", value=67.1, threshold=65.0,
|
||||
status="resolved", resolve_note="设备恢复正常",
|
||||
triggered_at=now - timedelta(days=1, hours=14),
|
||||
resolved_at=now - timedelta(days=1, hours=13, minutes=30),
|
||||
),
|
||||
# --- 1 day ago: HP overload (acknowledged, still active) ---
|
||||
AlarmEvent(
|
||||
rule_id=r_hp_overload.id, device_id=all_devices["HP-04"],
|
||||
severity="critical", title="热泵功率过载告警",
|
||||
description="当前值 39.5,阈值 38.0", value=39.5, threshold=38.0,
|
||||
status="acknowledged",
|
||||
triggered_at=now - timedelta(hours=18),
|
||||
acknowledged_at=now - timedelta(hours=17),
|
||||
),
|
||||
# --- 12 hours ago: Meter communication (active) ---
|
||||
AlarmEvent(
|
||||
rule_id=r_meter.id, device_id=all_devices["METER-PUMP"],
|
||||
severity="critical", title="电表通信中断告警",
|
||||
description="当前值 0.0,阈值 0.0", value=0.0, threshold=0.0,
|
||||
status="active",
|
||||
triggered_at=now - timedelta(hours=12),
|
||||
),
|
||||
# --- 6 hours ago: Temperature out of range (active) ---
|
||||
AlarmEvent(
|
||||
rule_id=r_temp.id, device_id=all_devices["TH-02"],
|
||||
severity="warning", title="室内温度超限告警",
|
||||
description="当前值 31.8,阈值 [16.0, 30.0]", value=31.8, threshold=30.0,
|
||||
status="active",
|
||||
triggered_at=now - timedelta(hours=6),
|
||||
),
|
||||
# --- 2 hours ago: PV low (active) ---
|
||||
AlarmEvent(
|
||||
rule_id=r_pv_low.id, device_id=all_devices["INV-02"],
|
||||
severity="warning", title="光伏功率过低告警",
|
||||
description="当前值 3.1,阈值 5.0", value=3.1, threshold=5.0,
|
||||
status="active",
|
||||
triggered_at=now - timedelta(hours=2),
|
||||
),
|
||||
# --- 30 min ago: HP COP low (active) ---
|
||||
AlarmEvent(
|
||||
rule_id=r_hp_cop.id, device_id=all_devices["HP-02"],
|
||||
severity="major", title="热泵COP过低告警",
|
||||
description="当前值 1.7,阈值 2.0", value=1.7, threshold=2.0,
|
||||
status="active",
|
||||
triggered_at=now - timedelta(minutes=30),
|
||||
),
|
||||
]
|
||||
session.add_all(alarm_events)
|
||||
|
||||
await session.commit()
|
||||
|
||||
print("=" * 60)
|
||||
print("Seed data inserted successfully!")
|
||||
print(f" - {len(roles)} roles")
|
||||
print(f" - {len(users)} users (admin/admin123)")
|
||||
print("=" * 60)
|
||||
print(f" - {len(roles)} roles (with permissions)")
|
||||
print(f" - {len(users)} users (admin/admin123, energy_mgr/tianpu123, operator1/tianpu123)")
|
||||
print(f" - {len(device_types)} device types")
|
||||
print(f" - {len(groups)} device groups")
|
||||
print(f" - {len(groups)} device groups (hierarchical)")
|
||||
print(f" - {len(devices)} devices")
|
||||
print(f" - {len(alarm_rules)} alarm rules")
|
||||
print(f" - {len(factors)} emission factors")
|
||||
print(f" - {len(templates)} report templates")
|
||||
print(f" - {len(alarm_events)} alarm events (historical)")
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user