diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..579ffbf --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# 天普大兴园区 EMS 环境配置 +CUSTOMER=tianpu +DATABASE_URL=sqlite+aiosqlite:///./tianpu_ems.db +REDIS_URL=redis://localhost:6379/0 +REDIS_ENABLED=true +USE_SIMULATOR=true +AGGREGATION_ENABLED=true +INGESTION_QUEUE_ENABLED=false +TIMESCALE_ENABLED=false +SECRET_KEY=tianpu-change-this-in-production +SMTP_ENABLED=false diff --git a/README.md b/README.md new file mode 100644 index 0000000..9759e5e --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# 天普大兴园区 智慧能源管理平台 (tp-ems) + +## 项目说明 +天普大兴园区EMS客户定制项目,基于 ems-core 标准产品。 + +## 目录结构 +- `core/` — EMS核心代码(通过git subtree引入,勿直接修改) +- `customers/tianpu/` — 天普专属配置(设备、计费、品牌) +- `scripts/` — 天普数据初始化脚本 +- `.env.example` — 环境变量模板 + +## 快速开始 +1. 复制环境配置:`cp .env.example core/backend/.env` +2. 安装后端依赖:`cd core/backend && pip install -r requirements.txt` +3. 初始化数据库:`cd core/backend && python -m alembic upgrade head` +4. 导入种子数据:`cd scripts && python seed_data.py`(需先cd回项目根目录的scripts) + + 或者:`cd 项目根目录 && python scripts/seed_data.py` +5. 启动后端:`cd core/backend && python -m uvicorn app.main:app --port 8000 --reload` +6. 启动前端:`cd core/frontend && npm install && npm run dev` +7. 访问:http://localhost:3000(admin / admin123) + +## 更新核心代码 +当 ems-core 发布新版本时: +``` +git subtree pull --prefix=core http://192.168.1.77:3300/tianpu/ems-core.git v1.1.0 --squash +``` + +## 当前核心版本 +查看 `core/VERSION` 文件。 diff --git a/customers/tianpu/config.yaml b/customers/tianpu/config.yaml new file mode 100644 index 0000000..b5bfbdb --- /dev/null +++ b/customers/tianpu/config.yaml @@ -0,0 +1,17 @@ +# 天普大兴园区 - 客户配置 +customer_name: "天普新能源" +platform_name: "天普零碳园区智慧能源管理平台" +platform_name_en: "Tianpu Zero-Carbon Park Smart EMS" +logo_url: "/static/logo-tianpu.png" +theme_color: "#1890ff" +cors_origins: + - "http://localhost:3000" + - "http://localhost:5173" +collectors: + - modbus_tcp + - mqtt + - http_api +features: + charging: true + carbon: true + bigscreen_3d: true diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..1902643 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,16 @@ +# 天普大兴园区 — Docker Compose Override +# Usage: docker compose -f core/docker-compose.yml -f docker-compose.override.yml up +version: '3.8' +services: + backend: + build: + context: ./core/backend + environment: + - CUSTOMER=tianpu + volumes: + - ./customers/tianpu:/app/customers/tianpu:ro + - ./scripts:/app/scripts:ro + + frontend: + build: + context: ./core/frontend diff --git a/scripts/seed_data.py b/scripts/seed_data.py new file mode 100644 index 0000000..2c9a4a9 --- /dev/null +++ b/scripts/seed_data.py @@ -0,0 +1,1008 @@ +"""种子数据 - 天普园区设备信息、用户、告警规则、碳排放因子、报表模板、历史告警""" +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__), "..", "core", "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 +from app.models.energy import EnergyCategory +from app.models.charging import ( + ChargingMerchant, ChargingBrand, ChargingStation, + ChargingPile, ChargingPriceStrategy, ChargingPriceParam, ChargingOrder, +) +from app.models.quota import EnergyQuota +from app.models.pricing import ElectricityPricing, PricingPeriod +from app.models.maintenance import InspectionPlan, InspectionRecord, RepairOrder, DutySchedule +from app.models.management import Regulation, Standard, ProcessDoc, EmergencyPlan + + +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"), + User(username="visitor", hashed_password=hash_password("visitor123"), + full_name="访客", role="visitor", email=None, + phone=None), + ] + 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="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), + 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.flush() + + # ===================================================================== + # 10. 能耗分项类别 (5 categories) + # ===================================================================== + categories = [ + EnergyCategory(name="暖通空调", code="hvac", sort_order=1, icon="fire", color="#FF6B6B"), + EnergyCategory(name="照明", code="lighting", sort_order=2, icon="bulb", color="#FFD93D"), + EnergyCategory(name="动力", code="power", sort_order=3, icon="thunderbolt", color="#6BCB77"), + EnergyCategory(name="特殊", code="special", sort_order=4, icon="experiment", color="#4D96FF"), + EnergyCategory(name="其他", code="other", sort_order=5, icon="more", color="#9B59B6"), + ] + session.add_all(categories) + await session.flush() + + # Map category codes to IDs + cat_map = {c.code: c.id for c in categories} + + # Assign categories to existing devices + for code, dev_id in all_devices.items(): + dev = await session.get(Device, dev_id) + if code.startswith("HP-"): + dev.category_id = cat_map["hvac"] + elif code.startswith("METER-") or code.startswith("HM-"): + dev.category_id = cat_map["power"] + elif code.startswith("TH-") or code.startswith("WM-"): + dev.category_id = cat_map["other"] + elif code.startswith("INV-"): + dev.category_id = cat_map["special"] + await session.flush() + + # ===================================================================== + # 11. 充电桩管理 (merchant, brands, stations, piles, pricing, orders) + # ===================================================================== + merchant = ChargingMerchant( + name="天普充电运营", contact_person="张运营", + phone="13800001111", email="charging@tianpu.com", + status="active", settlement_type="postpaid", + ) + session.add(merchant) + await session.flush() + + brand1 = ChargingBrand(brand_name="特来电", country="中国", description="国内领先充电桩品牌") + brand2 = ChargingBrand(brand_name="星星充电", country="中国", description="新能源充电设备") + session.add_all([brand1, brand2]) + await session.flush() + + station1 = ChargingStation( + name="A栋地下停车场充电站", merchant_id=merchant.id, type="public", + address="天普大兴园区A栋B1", status="active", + total_piles=4, available_piles=2, total_power_kw=480, + operating_hours="00:00-24:00", created_by=1, + ) + station2 = ChargingStation( + name="园区露天充电站", merchant_id=merchant.id, type="public", + address="天普大兴园区南门", status="active", + total_piles=2, available_piles=1, total_power_kw=240, + operating_hours="06:00-23:00", created_by=1, + ) + session.add_all([station1, station2]) + await session.flush() + + # 6 charging piles + piles = [ + ChargingPile(station_id=station1.id, encoding="CP-A1-01", name="A栋1号桩", + type="DC_fast", brand="特来电", model="TLD-120", rated_power_kw=120, + connector_type="GB_T", status="active", work_status="idle"), + ChargingPile(station_id=station1.id, encoding="CP-A1-02", name="A栋2号桩", + type="DC_fast", brand="特来电", model="TLD-120", rated_power_kw=120, + connector_type="GB_T", status="active", work_status="charging"), + ChargingPile(station_id=station1.id, encoding="CP-A1-03", name="A栋3号桩", + type="AC_slow", brand="星星充电", model="XX-7", rated_power_kw=7, + connector_type="GB_T", status="active", work_status="idle"), + ChargingPile(station_id=station1.id, encoding="CP-A1-04", name="A栋4号桩", + type="DC_superfast", brand="特来电", model="TLD-240", rated_power_kw=240, + connector_type="GB_T", status="active", work_status="fault"), + ChargingPile(station_id=station2.id, encoding="CP-S1-01", name="南门1号桩", + type="DC_fast", brand="星星充电", model="XX-120", rated_power_kw=120, + connector_type="GB_T", status="active", work_status="idle"), + ChargingPile(station_id=station2.id, encoding="CP-S1-02", name="南门2号桩", + type="DC_fast", brand="星星充电", model="XX-120", rated_power_kw=120, + connector_type="GB_T", status="active", work_status="charging"), + ] + session.add_all(piles) + await session.flush() + + # Charging price strategy with 4 time periods + charge_strategy = ChargingPriceStrategy( + strategy_name="园区充电分时电价", station_id=station1.id, + bill_model="tou", description="按时段计费", status="active", + ) + session.add(charge_strategy) + await session.flush() + + charge_periods = [ + ChargingPriceParam(strategy_id=charge_strategy.id, start_time="10:00", end_time="15:00", + period_mark="peak", elec_price=1.2, service_price=0.8), + ChargingPriceParam(strategy_id=charge_strategy.id, start_time="18:00", end_time="21:00", + period_mark="peak", elec_price=1.2, service_price=0.8), + ChargingPriceParam(strategy_id=charge_strategy.id, start_time="07:00", end_time="10:00", + period_mark="flat", elec_price=0.7, service_price=0.6), + ChargingPriceParam(strategy_id=charge_strategy.id, start_time="23:00", end_time="07:00", + period_mark="valley", elec_price=0.3, service_price=0.4), + ] + session.add_all(charge_periods) + + # 5 sample charging orders + charging_orders = [ + ChargingOrder( + order_no="CHG20260401001", user_name="王先生", phone="13900001001", + station_id=station1.id, station_name="A栋地下停车场充电站", + pile_id=piles[0].id, pile_name="A栋1号桩", + start_time=now - timedelta(days=2, hours=14), + end_time=now - timedelta(days=2, hours=12, minutes=30), + car_no="京A12345", charge_method="app", settle_type="normal", + pay_type="wechat", settle_time=now - timedelta(days=2, hours=12, minutes=30), + settle_price=45.60, paid_price=45.60, elec_amt=28.80, serve_amt=16.80, + order_status="completed", charge_duration=5400, energy=38.0, + start_soc=20.0, end_soc=85.0, order_source="miniprogram", + ), + ChargingOrder( + order_no="CHG20260401002", user_name="李女士", phone="13900001002", + station_id=station2.id, station_name="园区露天充电站", + pile_id=piles[4].id, pile_name="南门1号桩", + start_time=now - timedelta(days=1, hours=20), + end_time=now - timedelta(days=1, hours=18), + car_no="京B67890", charge_method="plug_and_charge", settle_type="normal", + pay_type="alipay", settle_time=now - timedelta(days=1, hours=18), + settle_price=52.30, paid_price=52.30, elec_amt=33.60, serve_amt=18.70, + order_status="completed", charge_duration=7200, energy=48.0, + start_soc=15.0, end_soc=92.0, order_source="app", + ), + ChargingOrder( + order_no="CHG20260402001", user_name="赵工", phone="13900001003", + station_id=station1.id, station_name="A栋地下停车场充电站", + pile_id=piles[1].id, pile_name="A栋2号桩", + start_time=now - timedelta(hours=2), + car_no="京C11111", charge_method="app", + order_status="charging", energy=12.5, + start_soc=30.0, order_source="miniprogram", + ), + ChargingOrder( + order_no="CHG20260402002", user_name="孙经理", phone="13900001004", + station_id=station2.id, station_name="园区露天充电站", + pile_id=piles[5].id, pile_name="南门2号桩", + start_time=now - timedelta(hours=5), + end_time=now - timedelta(hours=3), + car_no="京D22222", charge_method="card", settle_type="delayed", + settle_price=38.90, elec_amt=24.50, serve_amt=14.40, + order_status="pending_pay", charge_duration=7200, energy=32.0, + start_soc=25.0, end_soc=78.0, order_source="pc", + ), + ChargingOrder( + order_no="CHG20260402003", user_name="周师傅", phone="13900001005", + station_id=station1.id, station_name="A栋地下停车场充电站", + pile_id=piles[3].id, pile_name="A栋4号桩", + start_time=now - timedelta(hours=8), + end_time=now - timedelta(hours=7, minutes=15), + car_no="京E33333", charge_method="app", settle_type="abnormal", + order_status="failed", charge_duration=2700, energy=5.2, + start_soc=40.0, end_soc=45.0, + abno_cause="充电桩故障,自动断电", order_source="miniprogram", + ), + ] + session.add_all(charging_orders) + await session.flush() + + # ===================================================================== + # 12. 能源配额 (4 quotas) + # ===================================================================== + quotas = [ + EnergyQuota(name="东楼月度用电定额", target_type="building", target_id=2, + energy_type="electricity", period="monthly", quota_value=50000, + unit="kWh", warning_threshold_pct=80, alert_threshold_pct=95, + is_active=True, created_by=1), + EnergyQuota(name="西楼月度用电定额", target_type="building", target_id=3, + energy_type="electricity", period="monthly", quota_value=45000, + unit="kWh", warning_threshold_pct=80, alert_threshold_pct=95, + is_active=True, created_by=1), + EnergyQuota(name="热泵系统月度用电定额", target_type="device_group", target_id=5, + energy_type="electricity", period="monthly", quota_value=30000, + unit="kWh", warning_threshold_pct=75, alert_threshold_pct=90, + is_active=True, created_by=1), + EnergyQuota(name="园区年度总用电定额", target_type="building", target_id=1, + energy_type="electricity", period="yearly", quota_value=600000, + unit="kWh", warning_threshold_pct=80, alert_threshold_pct=95, + is_active=True, created_by=1), + ] + session.add_all(quotas) + await session.flush() + + # ===================================================================== + # 13. 电价策略 (1 TOU pricing with 4 periods) + # ===================================================================== + pricing = ElectricityPricing( + name="2026年工商业分时电价", energy_type="electricity", + pricing_type="tou", effective_from=datetime(2026, 1, 1, tzinfo=timezone.utc), + is_active=True, created_by=1, + ) + session.add(pricing) + await session.flush() + + pricing_periods = [ + PricingPeriod(pricing_id=pricing.id, period_name="peak", + start_time="10:00", end_time="15:00", price_per_unit=1.2), + PricingPeriod(pricing_id=pricing.id, period_name="peak", + start_time="18:00", end_time="21:00", price_per_unit=1.2), + PricingPeriod(pricing_id=pricing.id, period_name="flat", + start_time="07:00", end_time="10:00", price_per_unit=0.7), + PricingPeriod(pricing_id=pricing.id, period_name="flat", + start_time="15:00", end_time="18:00", price_per_unit=0.7), + PricingPeriod(pricing_id=pricing.id, period_name="valley", + start_time="23:00", end_time="07:00", price_per_unit=0.3), + PricingPeriod(pricing_id=pricing.id, period_name="shoulder", + start_time="21:00", end_time="23:00", price_per_unit=0.85), + ] + session.add_all(pricing_periods) + await session.flush() + + # ===================================================================== + # 14. 运维管理 (inspection plans, records, repair orders, duty schedules) + # ===================================================================== + plan1 = InspectionPlan( + name="热泵日常巡检", description="检查热泵运行状态、温度、COP等关键参数", + device_group_id=5, schedule_type="daily", inspector_id=3, + checklist=json.dumps([ + {"item": "检查运行温度", "type": "check", "required": True}, + {"item": "检查COP值", "type": "check", "required": True}, + {"item": "检查噪音振动", "type": "check", "required": True}, + {"item": "检查管路密封", "type": "check", "required": False}, + ]), + is_active=True, next_run_at=now + timedelta(hours=8), created_by=1, + ) + plan2 = InspectionPlan( + name="光伏系统周巡检", description="检查光伏板、逆变器、线缆等设备状态", + device_group_id=4, schedule_type="weekly", inspector_id=3, + checklist=json.dumps([ + {"item": "检查光伏板清洁度", "type": "check", "required": True}, + {"item": "检查逆变器运行状态", "type": "check", "required": True}, + {"item": "检查线缆连接", "type": "check", "required": True}, + {"item": "记录发电量数据", "type": "input", "required": True}, + ]), + is_active=True, next_run_at=now + timedelta(days=3), created_by=1, + ) + session.add_all([plan1, plan2]) + await session.flush() + + # 3 inspection records + inspection_records = [ + InspectionRecord( + plan_id=plan1.id, inspector_id=3, status="completed", + findings=json.dumps([ + {"item": "检查运行温度", "result": "正常", "note": "出水温度45℃"}, + {"item": "检查COP值", "result": "正常", "note": "COP=3.8"}, + {"item": "检查噪音振动", "result": "正常", "note": ""}, + {"item": "检查管路密封", "result": "正常", "note": ""}, + ]), + started_at=now - timedelta(days=1, hours=9), + completed_at=now - timedelta(days=1, hours=8, minutes=30), + ), + InspectionRecord( + plan_id=plan1.id, inspector_id=3, status="issues_found", + findings=json.dumps([ + {"item": "检查运行温度", "result": "异常", "note": "出水温度偏高52℃"}, + {"item": "检查COP值", "result": "异常", "note": "COP=1.9,低于正常值"}, + {"item": "检查噪音振动", "result": "正常", "note": ""}, + {"item": "检查管路密封", "result": "正常", "note": ""}, + ]), + started_at=now - timedelta(days=2, hours=9), + completed_at=now - timedelta(days=2, hours=8, minutes=45), + ), + InspectionRecord( + plan_id=plan2.id, inspector_id=3, status="pending", + started_at=now - timedelta(hours=1), + ), + ] + session.add_all(inspection_records) + await session.flush() + + # 4 repair orders + repair_orders = [ + RepairOrder( + code="WO-20260401-001", title="热泵机组2压缩机异响", + description="巡检发现热泵机组2运行时有异常噪音,COP偏低,需检修", + device_id=all_devices["HP-02"], priority="high", status="open", + cost_estimate=5000.0, created_by=1, + created_at=now - timedelta(days=2, hours=8), + ), + RepairOrder( + code="WO-20260401-002", title="循环水泵电表通信故障", + description="水泵电表持续12小时无数据上报,需现场检查通信模块", + device_id=all_devices["METER-PUMP"], priority="critical", status="assigned", + assigned_to=3, cost_estimate=800.0, created_by=1, + created_at=now - timedelta(hours=12), + assigned_at=now - timedelta(hours=11), + ), + RepairOrder( + code="WO-20260402-001", title="A栋4号充电桩故障维修", + description="充电桩充电中途自动断电,用户报修", + device_id=None, priority="high", status="in_progress", + assigned_to=3, cost_estimate=2000.0, created_by=1, + created_at=now - timedelta(hours=7), + assigned_at=now - timedelta(hours=6, minutes=30), + ), + RepairOrder( + code="WO-20260330-001", title="西楼逆变器风扇更换", + description="逆变器散热风扇老化,温度报警频繁,已更换新风扇", + device_id=all_devices["INV-03"], priority="medium", status="completed", + assigned_to=3, resolution="已更换散热风扇,测试运行正常", + cost_estimate=1200.0, actual_cost=980.0, created_by=1, + created_at=now - timedelta(days=4), + assigned_at=now - timedelta(days=4), + completed_at=now - timedelta(days=3, hours=16), + ), + ] + session.add_all(repair_orders) + + # 7 duty schedules (this week, starting from Monday) + # Calculate this week's Monday + days_since_monday = now.weekday() + this_monday = now - timedelta(days=days_since_monday) + this_monday = this_monday.replace(hour=0, minute=0, second=0, microsecond=0) + + duty_schedules = [ + DutySchedule(user_id=3, duty_date=this_monday, shift="day", area_id=1, notes="全区巡检"), + DutySchedule(user_id=3, duty_date=this_monday + timedelta(days=1), shift="day", area_id=5, notes="热泵系统重点巡检"), + DutySchedule(user_id=3, duty_date=this_monday + timedelta(days=2), shift="day", area_id=4, notes="光伏系统巡检"), + DutySchedule(user_id=2, duty_date=this_monday + timedelta(days=2), shift="on_call", area_id=1, notes="值班备勤"), + DutySchedule(user_id=3, duty_date=this_monday + timedelta(days=3), shift="day", area_id=6, notes="电力计量巡检"), + DutySchedule(user_id=3, duty_date=this_monday + timedelta(days=4), shift="day", area_id=1, notes="全区巡检"), + DutySchedule(user_id=2, duty_date=this_monday + timedelta(days=5), shift="on_call", area_id=1, notes="周末值班"), + ] + session.add_all(duty_schedules) + await session.flush() + + # ===================================================================== + # 15. 管理体系 (regulations, standards, process docs, emergency plans) + # ===================================================================== + regulations = [ + Regulation(title="园区能源管理制度", category="operation", + content="本制度适用于天普大兴园区所有能源设施的日常运行管理。各部门应严格执行节能措施,定期汇报能耗情况。", + effective_date=datetime(2026, 1, 1, tzinfo=timezone.utc), + status="active", created_by=1), + Regulation(title="设备安全操作规程", category="safety", + content="所有操作人员必须持证上岗。高压设备操作需两人以上。严禁带电作业。定期进行安全培训。", + effective_date=datetime(2026, 1, 1, tzinfo=timezone.utc), + status="active", created_by=1), + Regulation(title="碳排放管理办法", category="environment", + content="园区实行碳排放总量控制,每月核算碳排放量。光伏和热泵优先使用,减少电网购电。", + effective_date=datetime(2026, 3, 1, tzinfo=timezone.utc), + status="active", created_by=1), + ] + session.add_all(regulations) + + standards = [ + Standard(name="能源管理体系", code="ISO 50001:2018", type="international", + description="国际能源管理体系标准,系统化管理能源绩效", + compliance_status="in_progress", + review_date=datetime(2026, 6, 1, tzinfo=timezone.utc)), + Standard(name="能源管理体系要求", code="GB/T 23331-2020", type="national", + description="等同采用ISO 50001的国家标准", + compliance_status="compliant", + review_date=datetime(2026, 12, 1, tzinfo=timezone.utc)), + Standard(name="公共建筑节能设计标准", code="GB 50189-2015", type="national", + description="公共建筑节能设计的国家强制性标准", + compliance_status="compliant", + review_date=datetime(2026, 12, 1, tzinfo=timezone.utc)), + ] + session.add_all(standards) + + process_docs = [ + ProcessDoc(title="热泵系统操作手册", category="operation", + content="1. 开机前检查水压和电源;2. 设置目标温度;3. 启动压缩机;4. 监测运行参数;5. 记录运行日志。", + version="2.0", approved_by="张能源", + effective_date=datetime(2026, 1, 15, tzinfo=timezone.utc)), + ProcessDoc(title="光伏系统维护流程", category="maintenance", + content="1. 每月清洗光伏板;2. 检查逆变器运行状态;3. 检查线缆连接;4. 测量绝缘电阻;5. 记录发电数据对比。", + version="1.5", approved_by="张能源", + effective_date=datetime(2026, 2, 1, tzinfo=timezone.utc)), + ] + session.add_all(process_docs) + + emergency_plans = [ + EmergencyPlan( + title="停电应急预案", scenario="power_outage", + steps=json.dumps([ + {"step_number": 1, "action": "确认停电范围和原因", "responsible_person": "值班员", "contact": "13800000003"}, + {"step_number": 2, "action": "启动储能系统供电", "responsible_person": "运维工程师", "contact": "13800000003"}, + {"step_number": 3, "action": "通知园区各部门", "responsible_person": "园区管理员", "contact": "13800000001"}, + {"step_number": 4, "action": "联系供电局抢修", "responsible_person": "能源主管", "contact": "13800000002"}, + ]), + responsible_person="张能源", + review_date=datetime(2026, 7, 1, tzinfo=timezone.utc), + is_active=True, + ), + EmergencyPlan( + title="设备故障应急预案", scenario="equipment_failure", + steps=json.dumps([ + {"step_number": 1, "action": "立即切断故障设备电源", "responsible_person": "值班员", "contact": "13800000003"}, + {"step_number": 2, "action": "评估影响范围并隔离", "responsible_person": "运维工程师", "contact": "13800000003"}, + {"step_number": 3, "action": "启动备用设备", "responsible_person": "运维工程师", "contact": "13800000003"}, + {"step_number": 4, "action": "联系厂家技术支持", "responsible_person": "能源主管", "contact": "13800000002"}, + {"step_number": 5, "action": "填写故障报告", "responsible_person": "运维工程师", "contact": "13800000003"}, + ]), + responsible_person="李运维", + review_date=datetime(2026, 7, 1, tzinfo=timezone.utc), + is_active=True, + ), + ] + session.add_all(emergency_plans) + + 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)") + print(f" - {len(categories)} energy categories") + print(f" - 1 charging merchant, {len([brand1, brand2])} brands") + print(f" - {len([station1, station2])} charging stations, {len(piles)} piles") + print(f" - {len(charging_orders)} charging orders") + print(f" - {len(quotas)} energy quotas") + print(f" - 1 electricity pricing with {len(pricing_periods)} periods") + print(f" - {len([plan1, plan2])} inspection plans, {len(inspection_records)} records") + print(f" - {len(repair_orders)} repair orders") + print(f" - {len(duty_schedules)} duty schedules") + print(f" - {len(regulations)} regulations, {len(standards)} standards") + print(f" - {len(process_docs)} process docs, {len(emergency_plans)} emergency plans") + + await engine.dispose() + + +if __name__ == "__main__": + asyncio.run(seed())