储能仿真系统-介绍与使用指南
- 写在前面
- 一、它能做什么
- 二、5 分钟上手
- 2.1 环境要求
- 2.2 启动
- 2.3 默认端口
- 2.4 读一个点
- 2.5 闭环控制:6 步走
- 三、主电气接线图
- 在这里插入图片描述
- 四、故障注入:测试 EMS 的异常处理能力
- 4.1 使用方法
- 4.2 10 个故障场景
- 4.3 两类命令的职责
- 五、工况引擎:自动化调控测试
- 六、接入你的 EMS
- 6.1 Python 示例(30 分钟跑通)
- 6.2 C# 示例
- 6.3 接入步骤
- 七、版本与授权
- 八、写在结尾
- 延伸阅读(同系列博客)
写在前面
如果你正在自研 EMS,大概率会遇到一个问题:开环测试只能看遥测,验证不了调控策略对不对。搭实物测试场动辄百万级,大储规模更是不可能提供完善的测试环境。本文介绍一款储能仿真模拟器(EssSimulator),它在一台主机上模拟整个储能电站,通过 Modbus TCP 对外暴露与真机一致的接口,让你的 EMS 在无需实物的情况下完成闭环调控验证。
本文覆盖:它能做什么、怎么装、怎么用、怎么测异常工况。全程配截图位置说明,方便你对照操作。
一、它能做什么
一句话:用一台电脑仿真整个储能电站,让 EMS 像连真机一样连上来,读遥测、下发控制、跑闭环。
核心能力:
- 闭环控制验证:EMS 下发功率 → 仿真器按物理规律响应 → EMS 看到反馈,验证调控策略是否正确。
- 大储规模仿真:支持多单元 × 多 PCS × 多 BMS × 多簇拓扑,规模可配置。实物测试场根本搭不起的规模,这里改个配置就行。
- 物理仿真:含 AC 潮流前推回代、Q-U 反馈、黑启动 V/f、励磁涌流、SOC 积分、PCS 爬坡——寄存器背后的数值有因果链,不是随机数。
- 点表驱动:BMS / EMU / 电表点位由 CSV 定义,可替换适配不同方案。
- 故障注入:模拟设备掉线、通讯中断、数据异常,验证 EMS 的异常处理能力。
二、5 分钟上手
2.1 环境要求
- 社区版:无需安装 .NET,下载解压直接运行(自包含单文件)
- 开发版:.NET 8 SDK
2.2 启动
# 社区版(解压后)./EssSimulator# 开发版dotnet run启动后进入控制台可视化界面。
2.3 默认端口
以默认 2 单元社区版配置为例:
| 设备 | 端口 | 用途 |
|---|---|---|
电表simEm | 1500 | 读 PCC 功率/电压 |
BMS 路 1simBms1 | 1501 | 读 SOC/电压,写并网脉冲 |
BMS 路 2simBms2 | 1502 | 同上 |
EMU 单元 1simEmu1 | 1601 | 写 PCS 启停/功率/断路器 |
EMU 单元 2simEmu2 | 1602 | 同上 |
默认监听127.0.0.1,仅本机访问。远程访问走 SSH 隧道。
2.4 读一个点
# 读 BMS1 的 SOC(yc7,地址 10007,FC4,Scale 1000)mbpoll-0-t3-a1-r10007-c1-p1501127.0.0.12.5 闭环控制:6 步走
合单元断路器 → 启动 PCS → BMS 并网 → 读 SOC → 下发功率 → 停机# 1. 合单元断路器(EMU yx0,FC5)mbpoll-0-t0-a1-r1000-p1601-1127.0.0.11# 2. 启动 PCS1(EMU yx3,FC5)mbpoll-0-t0-a1-r1003-p1601-1127.0.0.11# 3. BMS 一键并网(BMS yx0,FC5)mbpoll-0-t0-a1-r1000-p1501-1127.0.0.11# 4. 读 SOC(BMS yc7,FC4)mbpoll-0-t3-a1-r10007-c1-p1501127.0.0.1# 5. 下发有功 500kW(EMU yt0,FC6,Scale 10 → 写 5000)mbpoll-0-t4-a1-r40000-p1601-1127.0.0.15000# 6. 停机mbpoll-0-t4-a1-r40000-p1601-1127.0.0.10mbpoll-0-t0-a1-r1003-p1601-1127.0.0.10三、主电气接线图
启动后选「主电气接线」进入可视化界面,展示全站拓扑:
- 220kV 纵向主回路:电网 → 主断/电表/主变
- 35kV 母线并联:负载 + 各储能单元并列挂接
- 每个单元:单元断 → 单元变 → 690V 母线 → PCS-A/B → BMS-A/B
- 连接线表示电气通道(
│纵向主干、┬┴母线分支、│DC电池耦合)
四、故障注入:测试 EMS 的异常处理能力
这是 EMS 自研团队最关心的功能。模拟器提供 10 个标准故障场景,一键执行,验证 EMS 对设备掉线、通讯中断、数据异常的处理能力。
4.1 使用方法
在控制台命令输入菜单输入:
dpctest list autotest_fault_injection.json执行某个场景:
dpctest bms_offline_reconnect autotest_fault_injection.json4.2 10 个故障场景
设备掉线类(真通讯断开):
| 场景 | 模拟内容 | EMS 应验证 |
|---|---|---|
bms_offline_reconnect | BMS1 掉线 15s 后恢复 | 掉线检测、告警上报、恢复后数据刷新 |
pcs_control_lost | PCS 控制通道断开 15s 后恢复 | 指令重发机制、控制状态同步 |
em_offline_reconnect | 电表掉线 15s 后恢复 | 计量数据丢失处理、PCC 监控降级 |
multi_bms_offline | BMS1+BMS2 同时掉线 20s | 批量故障告警、告警风暴抑制 |
bms_restart | BMS1 短暂掉线 2s 后恢复 | 设备重启识别、快速重连 |
intermittent_disconnect | BMS1 反复掉线/上线 3 轮 | 通讯抖动处理、连接去抖 |
full_station_blackout | 全站设备同时掉线 20s | 全站失联处理、恢复顺序 |
数据异常类(通讯正常但值异常):
| 场景 | 模拟内容 | EMS 应验证 |
|---|---|---|
em_frequency_drift | 频率从 50→49.5 逐步漂移 | 频率越限告警、低频保护 |
em_power_jump | 有功从 -500→0→1000 突变 | 功率突变检测、防误判 |
bms_soc_freeze | SOC 强制固定 50% 持续 60s | 数据停滞检测、SOC 异常告警 |
[截图位置 10:EMS 侧观测到设备掉线]
建议截图内容:在 EMS 端(或 mbpoll 读点时)看到 BMS1 的数据突然不更新或连接报错。
说服力:证明仿真器的掉线对 EMS 端是真 TCP 断开,不是数据不变。
4.3 两类命令的职责
| 命令 | 职责 | 示例 |
|---|---|---|
esscmd link | 设备离线/上线(通讯断开) | esscmd link bms1 off |
dpc | 改点位值(数据异常注入) | dpc simEm.yc19 set 49.95 |
五、工况引擎:自动化调控测试
除了故障注入,模拟器还内置 8 个标准调控工况,用于验证 EMS 的正常调控能力:
| 工况 | 验证什么 |
|---|---|
| 功率阶跃-充电 | 合闸→启动→并网→下发充电功率→断言有功跟踪到位 |
| 功率阶跃-放电 | 同上但反方向 |
| 充放电循环 | 充电→搁置→放电→搁置,验证 SOC 往返与能量计量 |
| 离网/并网切换 | 并网运行→主断分→离网→主断合→恢复 |
| 黑启动 | 主断分→PCS 黑启动→软启动建压→断言母线带电 |
| SOC 均衡 | 多路 BMS 不同 SOC 下小功率运行 |
| 低电压穿越 | 电网电压跌落→PCS 保持并网→恢复 |
| AGC 跟随 | 模拟 AGC 指令序列,验证功率跟踪精度 |
工况跑完自动导出 CSV 报告(步骤结果 + 时间序列 + 关键指标)。
六、接入你的 EMS
6.1 Python 示例(30 分钟跑通)
""" EssSimulator EMS 接入示例(Python / pymodbus)30分钟快速上手:1. 安装依赖:pipinstallpymodbus2. 启动 EssSimulator(dotnet run,默认127.0.0.1)3. 运行本脚本:python quickstart.py 本脚本演示完整的闭环控制流程: 合主断/单元断 → 启动 PCS → BMS 并网 → 下发功率 → 读取反馈 → 停机 默认连接本地8单元配置(EMU 端口1601,BMS 端口1501)。"""importtimeimportsys from pymodbus.clientimportModbusTcpClient# ---------- 配置 ----------HOST="127.0.0.1"EMU_PORT=1601# EMU 单元 1(承载 PCS1/PCS2 控制)BMS_PORT=1501# BMS 路 1(承载并网控制)SLAVE_ID=1# EMU 控制点(来自 emu.csv,注意 -0 偏移:pymodbus 地址 = CSV Address - 1)EMU_UNIT_BREAKER=1000# yx0 高压断路器开合(FC5 线圈)EMU_PCS1_START=1003# yx3 PCS1 启停(FC5 线圈)EMU_PCS1_POWER=40000# yt0 PCS1 有功功率设置值(FC6 保持寄存器,Scale=10)# BMS 控制点(来自 bms_bank.csv)BMS_GRID_CONNECT=1000# yx0 一键并网脉冲(FC5 线圈)# BMS 遥测点(来自 bms_bank.csv,FC4 输入寄存器)BMS_SOC_ADDR=10007# yc7 系统SOC(Scale=1000,u16)def write_coil(client, address, value):"""写线圈(FC5)。pymodbus 用0-based 地址,CSV 是1-based,需 -1。""" rr=client.write_coil(address -1, bool(value),slave=SLAVE_ID)ifrr.isError(): raise RuntimeError(f"写线圈 {address} 失败: {rr}")print(f" 写线圈 {address} = {int(value)}")def write_register(client, address, value):"""写保持寄存器(FC6)。""" rr=client.write_register(address -1, int(value),slave=SLAVE_ID)ifrr.isError(): raise RuntimeError(f"写寄存器 {address} 失败: {rr}")print(f" 写寄存器 {address} = {int(value)}")def read_input_register(client, address,count=1):"""读输入寄存器(FC4)。""" rr=client.read_input_registers(address -1, count,slave=SLAVE_ID)ifrr.isError(): raise RuntimeError(f"读输入寄存器 {address} 失败: {rr}")returnrr.registers[0]ifcount==1elserr.registers def read_holding_register(client, address,count=1):"""读保持寄存器(FC3)。""" rr=client.read_holding_registers(address -1, count,slave=SLAVE_ID)ifrr.isError(): raise RuntimeError(f"读保持寄存器 {address} 失败: {rr}")returnrr.registers[0]ifcount==1elserr.registers def main(): emu=ModbusTcpClient(HOST, EMU_PORT,timeout=5)bms=ModbusTcpClient(HOST, BMS_PORT,timeout=5)ifnot emu.connect(): print(f"无法连接 EMU {HOST}:{EMU_PORT},请确认 EssSimulator 已启动")sys.exit(1)ifnot bms.connect(): print(f"无法连接 BMS {HOST}:{BMS_PORT}")sys.exit(1)print(f"已连接 EMU({EMU_PORT}) 和 BMS({BMS_PORT})\n")try:# 1. 合单元高压断路器print("[1/6] 合单元高压断路器")write_coil(emu, EMU_UNIT_BREAKER,1)time.sleep(1)# 2. 启动 PCS1print("[2/6] 启动 PCS1")write_coil(emu, EMU_PCS1_START,1)time.sleep(1)# 3. BMS 一键并网(脉冲)print("[3/6] BMS 一键并网")write_coil(bms, BMS_GRID_CONNECT,1)time.sleep(2)# 4. 读取当前 SOCprint("[4/6] 读取 BMS SOC")raw_soc=read_input_register(bms, BMS_SOC_ADDR)soc=raw_soc /1000.0# Scale=1000print(f" 当前 SOC = {soc:.3f} (raw={raw_soc})")# 5. 下发有功功率 500kW(Scale=10 → 寄存器值 5000)print("[5/6] 下发 PCS1 有功 500kW")target_kw=500write_register(emu, EMU_PCS1_POWER, target_kw *10)time.sleep(5)# 6. 停机print("[6/6] 停机")write_register(emu, EMU_PCS1_POWER,0)time.sleep(1)write_coil(emu, EMU_PCS1_START,0)print("\n闭环示例完成。")except Exception as e: print(f"\n执行异常: {e}",file=sys.stderr)sys.exit(1)finally: emu.close()bms.close()if__name__=="__main__":main()示例代码完成完整闭环:合断 → 启动 PCS → BMS 并网 → 读 SOC → 下发功率 → 停机。
6.2 C# 示例
// EssSimulator EMS 接入示例(C# / NModbus) // // 30 分钟快速上手: // 1. 创建一个控制台项目,NuGet 安装 NModbus // 2. 把本文件复制进去,dotnet run // // 本示例演示完整闭环:合断 → 启动 PCS → BMS 并网 → 下发功率 → 读反馈 → 停机 using System; using System.Threading.Tasks; using NModbus; namespace EmsQuickstartCSharp { class Program { const string Host = "127.0.0.1"; const int EmuPort = 1601; const int BmsPort = 1501; const byte SlaveId = 1; // EMU 控制点(emu.csv,地址为 1-based,NModbus 需 -1) const int EmuUnitBreaker = 1000; // yx0 高压断路器(FC5 线圈) const int EmuPcs1Start = 1003; // yx3 PCS1 启停(FC5 线圈) const int EmuPcs1Power = 40000; // yt0 PCS1 有功设置(FC6 保持寄存器,Scale=10) // BMS 控制点(bms_bank.csv) const int BmsGridConnect = 1000; // yx0 一键并网脉冲(FC5 线圈) // BMS 遥测点 const int BmsSocAddr = 10007; // yc7 系统SOC(FC4 输入寄存器,Scale=1000) static async Task Main(string[] args) { var emuFactory = new NModbus.ModbusFactory(); var bmsFactory = new NModbus.ModbusFactory(); using var emuTcp = new System.Net.Sockets.TcpClient(Host, EmuPort); using var bmsTcp = new System.Net.Sockets.TcpClient(Host, BmsPort); var emuMaster = emuFactory.CreateMaster(emuTcp); var bmsMaster = bmsFactory.CreateMaster(bmsTcp); Console.WriteLine($"已连接 EMU({EmuPort}) 和 BMS({BmsPort})\n"); // 1. 合单元高压断路器 Console.WriteLine("[1/6] 合单元高压断路器"); await emuMaster.WriteCoilAsync(SlaveId, EmuUnitBreaker - 1, true); await Task.Delay(1000); // 2. 启动 PCS1 Console.WriteLine("[2/6] 启动 PCS1"); await emuMaster.WriteCoilAsync(SlaveId, EmuPcs1Start - 1, true); await Task.Delay(1000); // 3. BMS 一键并网(脉冲) Console.WriteLine("[3/6] BMS 一键并网"); await bmsMaster.WriteCoilAsync(SlaveId, BmsGridConnect - 1, true); await Task.Delay(2000); // 4. 读取 SOC Console.WriteLine("[4/6] 读取 BMS SOC"); var socRaw = (await bmsMaster.ReadInputRegistersAsync(SlaveId, BmsSocAddr - 1, 1))[0]; double soc = socRaw / 1000.0; Console.WriteLine($" 当前 SOC = {soc:F3} (raw={socRaw})"); // 5. 下发有功 500kW(Scale=10 → 5000) Console.WriteLine("[5/6] 下发 PCS1 有功 500kW"); await emuMaster.WriteSingleRegisterAsync(SlaveId, EmuPcs1Power - 1, 500 * 10); await Task.Delay(5000); // 6. 停机 Console.WriteLine("[6/6] 停机"); await emuMaster.WriteSingleRegisterAsync(SlaveId, EmuPcs1Power - 1, 0); await Task.Delay(1000); await emuMaster.WriteCoilAsync(SlaveId, EmuPcs1Start - 1, false); Console.WriteLine("\n闭环示例完成。"); } } }samples/ems-csharp/Quickstart.cs,用 NModbus,逻辑同 Python 版。
6.3 接入步骤
- 配置你的 EMS Modbus 主站指向模拟器 IP 和端口
- 按你的 EMS 点表映射读写点位(点表不同可替换 CSV)
- 先跑开环:只读遥测,确认数据合理
- 再跑闭环:下发控制,观察反馈
- 跑故障注入场景,验证异常处理
- 跑工况引擎,对比你的 EMS 响应与预期
七、版本与授权
| 能力 | 社区版 | 标准版 | 企业版 |
|---|---|---|---|
| 储能单元数 | 2 | 8 | 不限 |
| 闭环控制 | ✓ | ✓ | ✓ |
| 故障注入套件 | ✓ | ✓ | ✓ |
| 工况库 | 基础 | 全部 | 全部+自定义 |
| 点表自定义 | ✗ | ✓ | ✓ |
| 商业使用 | ✗ | ✓ | ✓ |
社区版无 License 时自动进入 14 天试用模式(限 2 单元)。如需商业授权,请联系我!
- 社区版下载链接
八、写在结尾
开环测试只能证明"接口通了",闭环测试才能证明"调控策略对了"。这款工具的存在意义,就是让自研 EMS 团队在投入真机联调前,先在软件环境里把闭环跑通,把异常处理验证到位——省掉的不是测试本身,而是百万级实物测试场的代价和"上了现场才发现策略有 bug"的风险。
天道酬勤,与君共勉!
延伸阅读(同系列博客)
- 《储能仿真系统-困境和思考》
- 《储能仿真系统-需求和设计》
- 《储能仿真系统-功能和用途》
- 《储能仿真系统-真实和虚拟》
- 《储能仿真系统-总结和展望》(暂无)