zpark-ems v1.0.0: 中关村医疗器械园EMS客户项目

基于 ems-core v1.0.0,包含:
- customers/zpark/config.yaml — Z-Park品牌配置(阳光电源采集器)
- customers/zpark/devices.json — 10台逆变器 + 8台汇流箱设备清单
- customers/zpark/pricing.json — 北京2026年分时电价
- scripts/seed_zpark.py — Z-Park设备和告警种子数据
- docker-compose.override.yml — Z-Park部署配置
- core/ — ems-core v1.0.0 (git subtree)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Du Wenbo
2026-04-04 18:19:31 +08:00
parent 1256992c85
commit d153f8e430
7 changed files with 630 additions and 0 deletions

19
.env.example Normal file
View File

@@ -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

40
README.md Normal file
View File

@@ -0,0 +1,40 @@
# 中关村医疗器械园 智慧能源管理平台 (zpark-ems)
## 项目说明
中关村医疗器械园EMS客户定制项目基于 ems-core 标准产品。
## 园区特点
- 光伏为主4,561块太阳能板分布在22+栋建筑
- 阳光电源组串式逆变器AP101-AP20810台
- 直流汇流箱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:3000admin / 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` 文件。

View File

@@ -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

View File

@@ -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": "阳光电源"
}
]
}

View File

@@ -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}
]
}

View File

@@ -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

189
scripts/seed_zpark.py Normal file
View File

@@ -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())