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:
Du Wenbo
2026-04-02 18:46:42 +08:00
parent 6a59f9af76
commit 36c53e0e7c
72 changed files with 7284 additions and 392 deletions

View File

@@ -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
View 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
View 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 "$@"

View File

@@ -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="", 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__":