Files
tianpu-ems/scripts/seed_data.py
Du Wenbo ef9b5d055f feat: add system settings, audit log, device detail, dark mode, i18n, email notifications
System Management:
- System Settings page with 8 configurable parameters (admin only)
- Audit Log page with filterable table (user, action, resource, date range)
- Audit logging wired into auth, devices, users, alarms, reports API handlers
- SystemSetting model + migration (002)

Device Detail:
- Dedicated /devices/:id page with 4 tabs (realtime, historical trends, alarm history, device info)
- ECharts historical charts with granularity/time range selectors
- Device name clickable in Devices and Monitoring tables → navigates to detail

Email & Scheduling:
- Email service with SMTP support (STARTTLS/SSL/plain)
- Alarm email notification with professional HTML template
- Report scheduler using APScheduler for cron-based auto-generation
- Scheduled report task seeded (daily at 8am)

UI Enhancements:
- Dark mode toggle (persisted to localStorage, Ant Design darkAlgorithm)
- Data comparison view in Analysis page (dual date range, side-by-side metrics)
- i18n framework (i18next) with zh/en translations for menu and common UI
- Language switcher in header (中文/English)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:42:22 +08:00

576 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""种子数据 - 天普园区设备信息、用户、告警规则、碳排放因子、报表模板、历史告警"""
import asyncio
import json
import sys
import os
from datetime import datetime, timezone, timedelta
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, ReportTask
async def seed():
async with async_session() as session:
# =====================================================================
# 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. 用户 (admin + 2 demo users)
# =====================================================================
users = [
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. 设备类型 (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"]),
DeviceType(code="heat_pump", name="空气源热泵", icon="heat-pump",
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"]),
DeviceType(code="meter", name="智能电表", icon="meter",
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"]),
DeviceType(code="water_meter", name="水表", icon="water-meter",
data_fields=["flow_rate", "total_flow"]),
]
session.add_all(device_types)
# =====================================================================
# 4. 设备分组 (hierarchical: Campus -> subsystems)
# =====================================================================
groups = [
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 FK constraints before inserting devices
await session.flush()
# =====================================================================
# 5. 天普实际设备 (19 devices total)
# =====================================================================
devices = [
# --- 光伏逆变器 - 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=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=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=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=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=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=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=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=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=6,
location="机房热泵控制柜", protocol="modbus_rtu", collect_interval=60,
connection_params={"dtu_id": "2225000003"}),
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=5,
location="机房中部主管", protocol="modbus_rtu", collect_interval=60,
connection_params={"dtu_id": "2225000001"}),
# --- 温湿度传感器 ---
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=7,
location="大楼一楼西厅", protocol="mqtt", collect_interval=60,
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=7,
location="大楼二楼东厅", protocol="mqtt", collect_interval=60,
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"}),
# --- 水表 ---
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. 告警规则
# =====================================================================
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", "email"],
notify_targets={"emails": ["admin@tianpu.com", "energy@tianpu.com"]},
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", "email"],
notify_targets={"emails": ["admin@tianpu.com"]},
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),
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),
EmissionFactor(name="热泵节能减排因子", energy_type="heat_pump_saving", factor=0.8843,
unit="kWh", scope=2, region="north_china",
source="相比电加热节省的电量", year=2023),
]
session.add_all(factors)
# =====================================================================
# 8. 预置报表模板
# =====================================================================
templates = [
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()
# =====================================================================
# 8b. 定时报表任务 (daily report at 8am)
# =====================================================================
report_tasks = [
ReportTask(
template_id=1, # 日报模板
name="每日能源运行日报",
schedule="0 8 * * *",
recipients=["admin@tianpu.com"],
export_format="xlsx",
status="pending",
is_active=True,
created_by=1,
),
]
session.add_all(report_tasks)
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("=" * 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 (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(report_tasks)} report tasks (scheduled)")
print(f" - {len(alarm_events)} alarm events (historical)")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(seed())