diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..13f96c1 --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +# 中关村医疗器械园 EMS 环境配置 +CUSTOMER=zpark +DATABASE_URL=sqlite+aiosqlite:///./zpark_ems.db +REDIS_URL=redis://localhost:6379/0 +REDIS_ENABLED=true +USE_SIMULATOR=false +AGGREGATION_ENABLED=true +INGESTION_QUEUE_ENABLED=false +TIMESCALE_ENABLED=false +SECRET_KEY=zpark-change-this-in-production +SMTP_ENABLED=false + +# 阳光电源 iSolarCloud API(数据采集用) +# SUNGROW_API_BASE=https://gateway.isolarcloud.com +# SUNGROW_APP_KEY=your_app_key +# SUNGROW_SYS_CODE=901 +# SUNGROW_ACCESS_KEY=your_access_key +# SUNGROW_ACCOUNT=your_phone_number +# SUNGROW_PASSWORD=your_password diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a9eb7c --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# 中关村医疗器械园 智慧能源管理平台 (zpark-ems) + +## 项目说明 +中关村医疗器械园EMS客户定制项目,基于 ems-core 标准产品。 + +## 园区特点 +- 光伏为主:4,561块太阳能板,分布在22+栋建筑 +- 阳光电源组串式逆变器:AP101-AP208,10台 +- 直流汇流箱:49台 +- 数据采集:通过阳光电源 iSolarCloud API + +## 目录结构 +- `core/` — EMS核心代码(通过git subtree引入,勿直接修改) +- `customers/zpark/` — Z-Park专属配置 + - `config.yaml` — 品牌配置和功能开关 + - `devices.json` — 阳光电源逆变器和汇流箱设备清单 + - `pricing.json` — 北京工商业分时电价 +- `scripts/` — Z-Park数据初始化脚本 +- `.env.example` — 环境变量模板(含阳光电源API配置) + +## 快速开始 +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. 导入种子数据:`python scripts/seed_zpark.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) + +## 阳光电源API配置 +在 `.env` 中填入阳光电源 iSolarCloud API 凭证后,设置 `USE_SIMULATOR=false` 即可接入真实数据。 + +## 更新核心代码 +当 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/zpark/config.yaml b/customers/zpark/config.yaml new file mode 100644 index 0000000..0e87146 --- /dev/null +++ b/customers/zpark/config.yaml @@ -0,0 +1,15 @@ +# 中关村医疗器械园 - 客户配置 +customer_name: "中关村医疗器械园" +platform_name: "中关村医疗器械园智慧能源管理平台" +platform_name_en: "Z-Park Medical Device Smart EMS" +logo_url: "/static/logo-zpark.png" +theme_color: "#52c41a" +cors_origins: + - "http://localhost:3000" + - "http://localhost:5173" +collectors: + - sungrow_api +features: + charging: false + carbon: true + bigscreen_3d: false diff --git a/customers/zpark/devices.json b/customers/zpark/devices.json new file mode 100644 index 0000000..9702291 --- /dev/null +++ b/customers/zpark/devices.json @@ -0,0 +1,338 @@ +{ + "customer": { + "name": "中关村医疗器械园", + "code": "zpark", + "location": "北京市海淀区" + }, + "device_types": [ + { + "code": "sungrow_inverter", + "name": "阳光电源组串式逆变器", + "icon": "solar-panel", + "data_fields": ["power", "daily_energy", "total_energy", "voltage", "current", "frequency", "temperature"] + }, + { + "code": "dc_combiner", + "name": "直流汇流箱", + "icon": "combiner-box", + "data_fields": ["voltage", "current", "power", "string_current"] + }, + { + "code": "pv_panel_group", + "name": "光伏组件组", + "icon": "pv-panel", + "data_fields": ["power", "energy"] + } + ], + "device_groups": [ + { + "name": "中关村医疗器械园", + "location": "北京市海淀区", + "description": "中关村医疗器械园光伏项目总节点", + "children": [ + { + "name": "一期-26号楼", + "location": "26号楼屋顶", + "description": "一期项目,26号楼屋顶光伏" + }, + { + "name": "二期-69号", + "location": "69号区域", + "description": "二期项目,69号区域多栋楼屋顶光伏", + "children": [ + {"name": "1#楼", "location": "1#楼屋顶"}, + {"name": "2#楼", "location": "2#楼屋顶"}, + {"name": "4#楼", "location": "4#楼屋顶"}, + {"name": "5#楼", "location": "5#楼屋顶"}, + {"name": "7#楼", "location": "7#楼屋顶"}, + {"name": "12#楼", "location": "12#楼屋顶"} + ] + } + ] + } + ], + "devices": [ + { + "name": "AP101组串式逆变器", + "code": "ZP-INV-AP101", + "device_type": "sungrow_inverter", + "group": "一期-26号楼", + "rated_power": 40, + "model": "SG40KTL-M", + "manufacturer": "阳光电源", + "protocol": "http_api", + "collect_interval": 900, + "connection_params": { + "api_base": "https://gateway.isolarcloud.com", + "app_key": "1BF313B6A9F919A6FB6A90BD43D23395", + "sys_code": "901", + "x_access_key": "qpthtsf287zvtmr6t3q9hsc0k70f3tay", + "user_account": "13911211695", + "user_password": "123456#ABC", + "ps_id": "", + "device_sn": "AP101" + } + }, + { + "name": "AP102组串式逆变器", + "code": "ZP-INV-AP102", + "device_type": "sungrow_inverter", + "group": "一期-26号楼", + "rated_power": 50, + "model": "SG50KTL-M", + "manufacturer": "阳光电源", + "protocol": "http_api", + "collect_interval": 900, + "connection_params": { + "api_base": "https://gateway.isolarcloud.com", + "app_key": "1BF313B6A9F919A6FB6A90BD43D23395", + "sys_code": "901", + "x_access_key": "qpthtsf287zvtmr6t3q9hsc0k70f3tay", + "user_account": "13911211695", + "user_password": "123456#ABC", + "ps_id": "", + "device_sn": "AP102" + } + }, + { + "name": "AP201组串式逆变器", + "code": "ZP-INV-AP201", + "device_type": "sungrow_inverter", + "group": "1#楼", + "rated_power": 130, + "model": "SG125HV", + "manufacturer": "阳光电源", + "protocol": "http_api", + "collect_interval": 900, + "connection_params": { + "api_base": "https://gateway.isolarcloud.com", + "app_key": "1BF313B6A9F919A6FB6A90BD43D23395", + "sys_code": "901", + "x_access_key": "qpthtsf287zvtmr6t3q9hsc0k70f3tay", + "user_account": "13911211695", + "user_password": "123456#ABC", + "ps_id": "", + "device_sn": "AP201" + } + }, + { + "name": "AP202组串式逆变器", + "code": "ZP-INV-AP202", + "device_type": "sungrow_inverter", + "group": "2#楼", + "rated_power": 260, + "model": "SG250HX", + "manufacturer": "阳光电源", + "protocol": "http_api", + "collect_interval": 900, + "connection_params": { + "api_base": "https://gateway.isolarcloud.com", + "app_key": "1BF313B6A9F919A6FB6A90BD43D23395", + "sys_code": "901", + "x_access_key": "qpthtsf287zvtmr6t3q9hsc0k70f3tay", + "user_account": "13911211695", + "user_password": "123456#ABC", + "ps_id": "", + "device_sn": "AP202" + } + }, + { + "name": "AP203组串式逆变器", + "code": "ZP-INV-AP203", + "device_type": "sungrow_inverter", + "group": "4#楼", + "rated_power": 160, + "model": "SG160HX", + "manufacturer": "阳光电源", + "protocol": "http_api", + "collect_interval": 900, + "connection_params": { + "api_base": "https://gateway.isolarcloud.com", + "app_key": "1BF313B6A9F919A6FB6A90BD43D23395", + "sys_code": "901", + "x_access_key": "qpthtsf287zvtmr6t3q9hsc0k70f3tay", + "user_account": "13911211695", + "user_password": "123456#ABC", + "ps_id": "", + "device_sn": "AP203" + } + }, + { + "name": "AP204组串式逆变器", + "code": "ZP-INV-AP204", + "device_type": "sungrow_inverter", + "group": "5#楼", + "rated_power": 400, + "model": "SG350HX", + "manufacturer": "阳光电源", + "protocol": "http_api", + "collect_interval": 900, + "connection_params": { + "api_base": "https://gateway.isolarcloud.com", + "app_key": "1BF313B6A9F919A6FB6A90BD43D23395", + "sys_code": "901", + "x_access_key": "qpthtsf287zvtmr6t3q9hsc0k70f3tay", + "user_account": "13911211695", + "user_password": "123456#ABC", + "ps_id": "", + "device_sn": "AP204" + } + }, + { + "name": "AP205组串式逆变器", + "code": "ZP-INV-AP205", + "device_type": "sungrow_inverter", + "group": "7#楼", + "rated_power": 290, + "model": "SG250HX", + "manufacturer": "阳光电源", + "protocol": "http_api", + "collect_interval": 900, + "connection_params": { + "api_base": "https://gateway.isolarcloud.com", + "app_key": "1BF313B6A9F919A6FB6A90BD43D23395", + "sys_code": "901", + "x_access_key": "qpthtsf287zvtmr6t3q9hsc0k70f3tay", + "user_account": "13911211695", + "user_password": "123456#ABC", + "ps_id": "", + "device_sn": "AP205" + } + }, + { + "name": "AP206组串式逆变器", + "code": "ZP-INV-AP206", + "device_type": "sungrow_inverter", + "group": "7#楼", + "rated_power": 300, + "model": "SG300HX", + "manufacturer": "阳光电源", + "protocol": "http_api", + "collect_interval": 900, + "connection_params": { + "api_base": "https://gateway.isolarcloud.com", + "app_key": "1BF313B6A9F919A6FB6A90BD43D23395", + "sys_code": "901", + "x_access_key": "qpthtsf287zvtmr6t3q9hsc0k70f3tay", + "user_account": "13911211695", + "user_password": "123456#ABC", + "ps_id": "", + "device_sn": "AP206" + } + }, + { + "name": "AP207组串式逆变器", + "code": "ZP-INV-AP207", + "device_type": "sungrow_inverter", + "group": "12#楼", + "rated_power": 280, + "model": "SG250HX", + "manufacturer": "阳光电源", + "protocol": "http_api", + "collect_interval": 900, + "connection_params": { + "api_base": "https://gateway.isolarcloud.com", + "app_key": "1BF313B6A9F919A6FB6A90BD43D23395", + "sys_code": "901", + "x_access_key": "qpthtsf287zvtmr6t3q9hsc0k70f3tay", + "user_account": "13911211695", + "user_password": "123456#ABC", + "ps_id": "", + "device_sn": "AP207" + } + }, + { + "name": "AP208组串式逆变器", + "code": "ZP-INV-AP208", + "device_type": "sungrow_inverter", + "group": "12#楼", + "rated_power": 290, + "model": "SG250HX", + "manufacturer": "阳光电源", + "protocol": "http_api", + "collect_interval": 900, + "connection_params": { + "api_base": "https://gateway.isolarcloud.com", + "app_key": "1BF313B6A9F919A6FB6A90BD43D23395", + "sys_code": "901", + "x_access_key": "qpthtsf287zvtmr6t3q9hsc0k70f3tay", + "user_account": "13911211695", + "user_password": "123456#ABC", + "ps_id": "", + "device_sn": "AP208" + } + }, + { + "name": "26号楼1#汇流箱", + "code": "ZP-CB-2601", + "device_type": "dc_combiner", + "group": "一期-26号楼", + "rated_power": 20, + "model": "PVS-16M", + "manufacturer": "阳光电源" + }, + { + "name": "26号楼2#汇流箱", + "code": "ZP-CB-2602", + "device_type": "dc_combiner", + "group": "一期-26号楼", + "rated_power": 20, + "model": "PVS-16M", + "manufacturer": "阳光电源" + }, + { + "name": "1#楼1#汇流箱", + "code": "ZP-CB-0101", + "device_type": "dc_combiner", + "group": "1#楼", + "rated_power": 30, + "model": "PVS-24M", + "manufacturer": "阳光电源" + }, + { + "name": "2#楼1#汇流箱", + "code": "ZP-CB-0201", + "device_type": "dc_combiner", + "group": "2#楼", + "rated_power": 50, + "model": "PVS-24M", + "manufacturer": "阳光电源" + }, + { + "name": "5#楼1#汇流箱", + "code": "ZP-CB-0501", + "device_type": "dc_combiner", + "group": "5#楼", + "rated_power": 60, + "model": "PVS-24M", + "manufacturer": "阳光电源" + }, + { + "name": "7#楼1#汇流箱", + "code": "ZP-CB-0701", + "device_type": "dc_combiner", + "group": "7#楼", + "rated_power": 50, + "model": "PVS-24M", + "manufacturer": "阳光电源" + }, + { + "name": "12#楼1#汇流箱", + "code": "ZP-CB-1201", + "device_type": "dc_combiner", + "group": "12#楼", + "rated_power": 50, + "model": "PVS-24M", + "manufacturer": "阳光电源" + }, + { + "name": "12#楼2#汇流箱", + "code": "ZP-CB-1202", + "device_type": "dc_combiner", + "group": "12#楼", + "rated_power": 50, + "model": "PVS-24M", + "manufacturer": "阳光电源" + } + ] +} diff --git a/customers/zpark/pricing.json b/customers/zpark/pricing.json new file mode 100644 index 0000000..d59c9bb --- /dev/null +++ b/customers/zpark/pricing.json @@ -0,0 +1,13 @@ +{ + "name": "2026年北京工商业分时电价", + "energy_type": "electricity", + "pricing_type": "tou", + "periods": [ + {"name": "peak", "start": "10:00", "end": "15:00", "price": 1.35}, + {"name": "peak", "start": "18:00", "end": "21:00", "price": 1.35}, + {"name": "flat", "start": "07:00", "end": "10:00", "price": 0.85}, + {"name": "flat", "start": "15:00", "end": "18:00", "price": 0.85}, + {"name": "valley", "start": "23:00", "end": "07:00", "price": 0.35}, + {"name": "shoulder", "start": "21:00", "end": "23:00", "price": 0.95} + ] +} diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..02ab11d --- /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=zpark + volumes: + - ./customers/zpark:/app/customers/zpark:ro + - ./scripts:/app/scripts:ro + + frontend: + build: + context: ./core/frontend diff --git a/scripts/seed_zpark.py b/scripts/seed_zpark.py new file mode 100644 index 0000000..c437b95 --- /dev/null +++ b/scripts/seed_zpark.py @@ -0,0 +1,189 @@ +"""种子数据 - 中关村医疗器械园光伏设备、告警规则、碳排放因子、电价配置""" +import asyncio +import json +import sys +import os + +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.models.device import Device, DeviceType, DeviceGroup +from app.models.alarm import AlarmRule +from app.models.carbon import EmissionFactor +from app.models.pricing import ElectricityPricing, PricingPeriod + + +# Path to device definitions +DEVICES_JSON = os.path.join(os.path.dirname(__file__), "..", "customers", "zpark", "devices.json") +PRICING_JSON = os.path.join(os.path.dirname(__file__), "..", "customers", "zpark", "pricing.json") + + +async def seed(): + with open(DEVICES_JSON, "r", encoding="utf-8") as f: + config = json.load(f) + + async with async_session() as session: + # ================================================================= + # 1. 设备类型 + # ================================================================= + for dt in config["device_types"]: + # Check if type already exists (may overlap with tianpu seed) + existing = await session.execute( + select(DeviceType).where(DeviceType.code == dt["code"]) + ) + if existing.scalar_one_or_none() is None: + session.add(DeviceType( + code=dt["code"], + name=dt["name"], + icon=dt.get("icon"), + data_fields=dt.get("data_fields"), + )) + await session.flush() + + # ================================================================= + # 2. 设备分组 (hierarchical) + # ================================================================= + group_name_to_id = {} + + async def create_groups(groups, parent_id=None): + for g in groups: + grp = DeviceGroup( + name=g["name"], + parent_id=parent_id, + location=g.get("location"), + description=g.get("description"), + ) + session.add(grp) + await session.flush() + group_name_to_id[g["name"]] = grp.id + if "children" in g: + await create_groups(g["children"], parent_id=grp.id) + + await create_groups(config["device_groups"]) + + # ================================================================= + # 3. 设备 + # ================================================================= + devices = [] + for d in config["devices"]: + group_id = group_name_to_id.get(d.get("group")) + device = Device( + name=d["name"], + code=d["code"], + device_type=d["device_type"], + group_id=group_id, + model=d.get("model"), + manufacturer=d.get("manufacturer"), + rated_power=d.get("rated_power"), + location=d.get("location", ""), + protocol=d.get("protocol", "http_api"), + connection_params=d.get("connection_params"), + collect_interval=d.get("collect_interval", 900), + status="offline", + is_active=True, + ) + devices.append(device) + session.add_all(devices) + await session.flush() + + # ================================================================= + # 4. 碳排放因子 (光伏减排) + # ================================================================= + # Check if PV generation factor already exists + existing_factor = await session.execute( + select(EmissionFactor).where(EmissionFactor.energy_type == "pv_generation") + ) + if existing_factor.scalar_one_or_none() is None: + session.add(EmissionFactor( + name="华北电网光伏减排因子", + energy_type="pv_generation", + factor=0.8843, + unit="kWh", + scope=2, + region="north_china", + source="等量替代电网电力", + year=2023, + )) + await session.flush() + + # ================================================================= + # 5. 告警规则 (逆变器监控) + # ================================================================= + alarm_rules = [ + AlarmRule( + name="逆变器功率过低告警", + device_type="sungrow_inverter", + data_type="power", + condition="lt", + threshold=1.0, + duration=1800, + severity="warning", + notify_channels=["app", "wechat"], + is_active=True, + ), + AlarmRule( + name="逆变器通信中断告警", + device_type="sungrow_inverter", + data_type="power", + condition="eq", + threshold=0.0, + duration=3600, + severity="critical", + notify_channels=["app", "sms", "wechat"], + is_active=True, + ), + AlarmRule( + name="逆变器过温告警", + device_type="sungrow_inverter", + data_type="temperature", + condition="gt", + threshold=70.0, + duration=120, + severity="major", + notify_channels=["app", "sms"], + is_active=True, + ), + ] + session.add_all(alarm_rules) + + # ================================================================= + # 6. 电价配置 + # ================================================================= + if os.path.exists(PRICING_JSON): + with open(PRICING_JSON, "r", encoding="utf-8") as f: + pricing_config = json.load(f) + + pricing = ElectricityPricing( + name=pricing_config["name"], + energy_type=pricing_config.get("energy_type", "electricity"), + pricing_type=pricing_config.get("pricing_type", "tou"), + is_active=True, + ) + session.add(pricing) + await session.flush() + + for period in pricing_config.get("periods", []): + session.add(PricingPeriod( + pricing_id=pricing.id, + period_name=period["name"], + start_time=period["start"], + end_time=period["end"], + price_per_unit=period["price"], + )) + + await session.commit() + print("Z-Park seed data created successfully!") + + # Print summary + dev_count = len(config["devices"]) + group_count = len(group_name_to_id) + print(f" - Device types: {len(config['device_types'])}") + print(f" - Device groups: {group_count}") + print(f" - Devices: {dev_count}") + print(f" - Alarm rules: {len(alarm_rules)}") + print(f" - Pricing periods: {len(pricing_config.get('periods', []))}") + + +if __name__ == "__main__": + asyncio.run(seed())