最近留意到工商业储能赛道的一条新动态:有企业完成数千万元融资,并且市场预期海外终端占比会在未来一段时间内持续走高。这类新闻更多是在讲资本和商业节奏,但作为技术人员,我更关注的是另一个问题:工商业储能项目真正落地时,系统里到底有哪些软件模块在协作?EMS 是怎么决定“什么时候充、什么时候放”的?
这篇文章不讨论融资数字,而是从工程视角拆解工商业储能系统的技术构成,重点讲清楚能量管理系统(EMS)的核心调度逻辑,并给出一个可运行的 Python 演示程序。你可以跟着代码跑一遍,理解峰谷套利、SOC 约束、防逆流控制等关键概念,然后再迁移到真实的 BMS/PCS 联调场景中。
1. 工商业储能系统的基本组成
1.1 从“大号充电宝”说起
很多非储能行业的朋友第一次听到“工商业储能”,第一反应是“大号充电宝”。这个说法不算错,但不完整。工商业储能系统本质上是一个集成了电池、功率变换、监控调度、消防温控等多个子系统的中小型电力系统,它服务的是工厂、园区、商场、充电站这类工商业用户。
它的核心价值体现在三个方面:
- 削峰填谷:在电价低谷时充电,在电价高峰时放电,赚取峰谷价差。
- 需量管理:通过储能放电压低最大需量,减少基本电费。
- 应急备电:在电网停电或限电时,为重要负荷提供短时供电。
一套典型的工商业储能系统,硬件上包括电池簇、电池管理系统(BMS)、储能变流器(PCS)、能量管理系统(EMS)、并网柜、变压器、温控与消防系统。软件侧的重点则在 EMS,因为它承担了数据采集、策略决策、状态监控、告警保护等功能。
1.2 数据流与核心模块
从数据流的角度看,EMS 处于整个系统的“大脑”位置。底层设备通过通信协议把数据送到 EMS,EMS 经过计算后下发控制指令给 PCS 和 BMS。
简单数据流如下:
电池簇 <-> BMS <-> EMS <-> PCS <-> 并网点/电网 ↑ 电表/气象/负荷数据其中:
- BMS 负责上报电池电压、电流、温度、SOC、SOH,并执行过压、过流、过温保护。
- PCS 负责交直流变换,执行 EMS 下发的有功功率指令。
- 电表负责采集并网点功率,用于防逆流和需量控制。
- EMS 则把上面所有数据汇聚起来,运行调度算法,输出充放电功率。
如果项目接入云平台,EMS 还需要通过 MQTT、HTTP 等方式把数据上传到云端,同时接收云端下发的远程调度计划。
1.3 常见商业模式
工商业储能的收益来源,决定了调度策略的走向。常见模式包括:
| 模式 | 收益逻辑 | 对调度策略的要求 |
|---|---|---|
| 峰谷套利 | 低充高放,赚取价差 | 需要根据电价时段精确控制充放电窗 |
| 需量管理 | 降低最大需量,减少基本电费 | 需要预测负荷峰值,提前放电 |
| 需求侧响应 | 响应电网调峰调频指令 | 需要快速响应外部指令 |
| 动态增容 | 在不扩容变压器的情况下增加用电容量 | 需要结合负荷曲线做功率限制 |
| 备电服务 | 停电时保障关键负荷 | 需要预留 SOC 和离网切换策略 |
不同商业模式的调度策略差别很大,但最基础的峰谷套利和防逆流逻辑,几乎是每个项目都必须实现的。
2. 环境准备与示例工程结构
2.1 运行环境说明
本文的示例代码使用 Python 编写,目标是模拟一个简单的 EMS 调度算法。版本方面不需要特别激进,Python 3.8 以上即可运行。代码只使用了标准库json和math,不需要额外安装第三方依赖。
python --version如果你在 Windows 上运行,可以用py -3替代python。示例中的时间粒度是“小时”,实际项目中常见的时间粒度是“分钟”或“秒”,但调度思路完全一致。
2.2 示例工程结构
为了便于理解,我建议按下面的目录结构组织示例工程:
ems_demo/ ├── config.json └── ems_scheduler.pyconfig.json:保存电池参数、电价时段、模拟场景参数。ems_scheduler.py:包含负荷光伏曲线生成、调度决策、结果统计。
这种方式把配置和代码分离,符合真实项目中配置外部化的习惯。当然,真实项目里一般会拆成更多模块,比如数据采集服务、策略服务、指令下发服务,但本文先聚焦调度算法本身。
3. EMS 核心调度逻辑拆解
3.1 调度目标与约束条件
工商业储能 EMS 的调度问题,本质上是一个带约束的能量优化问题。目标函数通常是“收益最大化”或“电费最小化”,约束条件包括:
- 电池 SOC 上下限约束。
- PCS 最大充放电功率约束。
- 并网点功率不能超过变压器容量。
- 防逆流约束,即储能放电不能导致电能倒送到电网。
- 电池温度、电流等安全约束。
如果简化到最核心的部分,调度算法只需要回答一个问题:在某个时刻,我应该充电、放电,还是待机?充放多少功率?
一个最简单的规则策略是:
- 如果当前是峰时段,且 SOC 高于下限,则优先放电。
- 如果当前是谷时段或平段,且 SOC 低于上限,则优先充电。
- 任何时候放电都不能超过当前负荷,防止逆流。
这个策略看起来简单,但在实际工程中非常实用,很多中小型项目就是基于类似的规则表运行的。
3.2 峰谷套利的定价与时段
峰谷套利依赖电价曲线。国内很多地区执行分时电价,一天内存在尖峰、高峰、平段、低谷等时段。以示例中的电价为参考:
| 时段 | 电价(元/kWh) | 含义 |
|---|---|---|
| 0:00 - 8:00 | 0.35 | 谷段,适合充电 |
| 8:00 - 10:00 | 0.70 | 平段,视 SOC 情况充电或待机 |
| 10:00 - 12:00 | 1.20 | 峰段,适合放电 |
| 12:00 - 14:00 | 0.70 | 平段 |
| 14:00 - 18:00 | 1.20 | 峰段,适合放电 |
| 18:00 - 22:00 | 0.70 | 平段 |
| 22:00 - 24:00 | 0.35 | 谷段,适合充电 |
在实际项目中,电价时段往往不是这样整齐划一,有些地区每天分为几十个时间段,有些地区还有季节差异。调度程序应该把电价时段做成配置项,而不是写死在代码里。
3.3 防逆流控制
防逆流是工商业储能项目现场非常常见的需求。很多地区不允许用户侧的储能系统向电网倒送电,因此 PCS 的放电功率必须小于等于当前负荷功率。
用公式表达就是:
P_discharge <= P_load - P_solar其中P_discharge是储能放电功率,P_load是负荷功率,P_solar是光伏功率。如果负荷和光伏恰好抵消,甚至光伏反送,那么储能只能待机或充电。
防逆流通常由并网点电表实时采集功率,EMS 把功率数据带回给调度策略,动态限制 PCS 输出。这个逻辑在下面示例代码中也会体现。
3.4 通信协议与数据点
真正的 EMS 还需要和 BMS、PCS、电表通信。最常见的方式是 Modbus RTU/TCP,大型系统也会使用 IEC 61850、CAN 等协议。
以 Modbus TCP 为例,EMS 需要读取的典型数据点包括:
| 设备 | 典型寄存器 | 含义 |
|---|---|---|
| 电表 | 功率、电压、电流 | 并网点实时功率 |
| BMS | SOC、总电压、总电流、温度 | 电池状态 |
| PCS | 有功功率指令、运行状态 | 充放电控制 |
通信调试中经常遇到字节序、系数转换、寄存器地址不一致的问题,后面会在常见问题中展开。
4. 完整实战:一个可运行的 EMS 调度程序
4.1 配置设计
先创建config.json,把电池参数、电价时段和模拟场景参数都放在配置里。这样做的好处是,换一个项目时只需要改配置,不需要改代码。
{ "battery": { "capacity_kwh": 200, "initial_soc": 50, "min_soc": 10, "max_soc": 90, "power_kw": 50, "efficiency": 0.92 }, "price": [ { "start": 0, "end": 8, "price": 0.35 }, { "start": 8, "end": 10, "price": 0.7 }, { "start": 10, "end": 12, "price": 1.2 }, { "start": 12, "end": 14, "price": 0.7 }, { "start": 14, "end": 18, "price": 1.2 }, { "start": 18, "end": 22, "price": 0.7 }, { "start": 22, "end": 24, "price": 0.35 } ], "simulation": { "peak_price_threshold": 1.0, "hours": 24 } }配置里的efficiency表示充放电综合效率,0.92 表示充电时输入 1kWh 电能,电池实际存入 0.92kWh;放电时输出 1kWh,电池内部需要释放约 1/0.92 kWh 的能量。实际项目中,效率和温度、电流、电池寿命状态相关,但这里先用固定值简化处理。
peak_price_threshold是判断峰时段的电价阈值。电价大于等于该值时,认为处于峰时,策略优先放电。
4.2 负荷与光伏曲线模拟
真实项目中负荷和光伏曲线来自电表或预测模型。这里为了演示,用数学函数模拟一天 24 小时的曲线。
import json import math def load_config(path="config.json"): with open(path, "r", encoding="utf-8") as f: return json.load(f) def get_price(config, hour): for item in config["price"]: if item["start"] <= hour < item["end"]: return item["price"] return 0.0 def generate_daily_curve(hours=24): load = [] solar = [] for h in range(hours): base = 40.0 morning_peak = 50.0 * math.exp(-((h - 9) ** 2) / 8) evening_peak = 60.0 * math.exp(-((h - 19) ** 2) / 6) load.append(round(base + morning_peak + evening_peak, 2)) if 6 <= h <= 18: s = 100.0 * math.sin(math.pi * (h - 6) / 12) solar.append(round(max(s, 0), 2)) else: solar.append(0.0) return load, solar这段代码模拟了典型的“双峰”负荷曲线,早上 9 点和晚上 19 点左右各有一个高峰,光伏出力集中在 6 点到 18 点之间,中午出力最大。
4.3 调度策略实现
下面是核心的调度决策函数。它根据当前净负荷、电价、SOC 以及电池参数,决定返回charge、discharge还是idle。
def choose_action(hour, net_load, price, soc, battery, is_peak): cap = battery["capacity_kwh"] min_soc = battery["min_soc"] max_soc = battery["max_soc"] power = battery["power_kw"] eff = battery["efficiency"] if net_load > 0: # 当前负荷大于光伏,系统处于“缺电”状态 if is_peak and soc > min_soc: # 峰时放电,但放电功率不能超过缺电功率,防止逆流 max_dis = min(power, net_load, (soc - min_soc) * cap / 100.0 * eff) return "discharge", round(max_dis, 2) elif not is_peak and soc < max_soc: # 非峰时充电,充电空间受 SOC 上限约束 max_chg = min(power, (max_soc - soc) * cap / 100.0 / eff) return "charge", round(max_chg, 2) else: return "idle", 0.0 else: # 光伏有剩余,优先利用剩余光伏充电,不向电网反向输电 surplus = -net_load if soc < max_soc: max_chg = min(power, surplus, (max_soc - soc) * cap / 100.0 / eff) return "charge", round(max_chg, 2) else: return "idle", 0.0这里有两个细节值得注意。
第一,放电时为什么取min(power, net_load, ...)?因为 PCS 功率不能超过额定值,也不能超过当前缺电负荷,否则会产生逆流。第二,充电和放电时 SOC 的计算方式不同:充电时电池实际存入的能量是功率 * 效率,放电时电池实际消耗的能量是功率 / 效率。
4.4 主流程与结果输出
主函数负责按小时循环,执行调度动作并累计电费。
def run_simulation(config): battery = config["battery"] hours = config["simulation"].get("hours", 24) threshold = config["simulation"]["peak_price_threshold"] load_curve, solar_curve = generate_daily_curve(hours) soc = battery["initial_soc"] cap = battery["capacity_kwh"] eff = battery["efficiency"] no_storage_cost = 0.0 storage_cost = 0.0 rows = [] for h in range(hours): price = get_price(config, h) net_load = load_curve[h] - solar_curve[h] is_peak = price >= threshold action, power = choose_action(h, net_load, price, soc, battery, is_peak) if action == "charge": soc += power * eff / cap * 100.0 grid_power = net_load + power elif action == "discharge": soc -= power / eff / cap * 100.0 grid_power = net_load - power else: grid_power = net_load soc = max(battery["min_soc"], min(battery["max_soc"], soc)) no_storage_cost += max(net_load, 0) * price storage_cost += max(grid_power, 0) * price rows.append((h, load_curve[h], solar_curve[h], price, soc, action, power, grid_power)) return rows, no_storage_cost, storage_cost def print_result(rows, no_storage_cost, storage_cost): print(f"{'Hour':<5}{'Load':<8}{'Solar':<8}{'Price':<7}{'SOC':<7}{'Action':<10}{'Power':<8}{'Grid':<8}") for r in rows[:6]: print(f"{r[0]:<5}{r[1]:<8}{r[2]:<8}{r[3]:<7}{r[4]:<7.1f}{r[5]:<10}{r[6]:<8}{r[7]:<8.2f}") print("...") for r in rows[18:]: print(f"{r[0]:<5}{r[1]:<8}{r[2]:<8}{r[3]:<7}{r[4]:<7.1f}{r[5]:<10}{r[6]:<8}{r[7]:<8.2f}") print(f"\n未配置储能时:当日购电费用 {no_storage_cost:.2f} 元") print(f"配置储能后:当日购电费用 {storage_cost:.2f} 元") print(f"节省电费:{no_storage_cost - storage_cost:.2f} 元") if __name__ == "__main__": cfg = load_config() result_rows, cost1, cost2 = run_simulation(cfg) print_result(result_rows, cost1, cost2)运行方式很简单:
python ems_scheduler.py预期会输出类似以下格式的表格:
Hour Load Solar Price SOC Action Power Grid 0 42.63 0.0 0.35 50.0 charge 50.0 92.63 1 42.63 0.0 0.35 73.0 charge 50.0 92.63 2 42.63 0.0 0.35 90.0 charge 50.0 92.63 ...同时会输出未配置储能和配置储能后的当日购电费用,以及节省电费。由于模拟参数不同,具体数值会有所差异,关键是观察储能是否在谷段充电、峰段放电。
4.5 输出结果与收益说明
从调度结果看,储能会在电价低谷时段把 SOC 充到上限附近,在电价高峰时段放电,从而降低从电网购买的高价电量。在中午光伏出力较大时,如果仍有 SOC 空间,也会利用光伏余电充电。
这个示例实现了最基础的“峰谷套利 + 防逆流”策略。真实项目的 EMS 会更复杂,比如:
- 增加需量预测,在负荷尖峰到来之前提前放电。
- 增加功率预测,让光伏余电优先给储能充电。
- 增加云端调度接口,接收电网侧的需求响应指令。
- 增加电池寿命模型,避免深度充放。
但核心逻辑依然是“在约束条件下,把低电价时段或者光伏富余时段的电量,转移到高电价时段使用”。
5. 现场联调常见问题与排查思路
5.1 常见问题清单
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| BMS 通信超时 | RS485 接线错误或 Modbus 地址不对 | 检查物理接线,用 Modbus 扫描工具验证寄存器地址 |
| SOC 跳变 | 电流采样异常或电芯不一致 | 先校准电流传感器,再检查 BMS 均衡策略 |
| PCS 无法执行调度指令 | 通信协议寄存器写保护未关闭 | 确认 PCS 控制模式,检查指令下发权限 |
| 防逆流功能不生效 | 并网点电表安装位置不对或方向接反 | 核对电表相序和功率方向,查看实时功率数值 |
| 调度策略不执行 | 电价时段配置错误或时钟不同步 | 检查 EMS 所在设备的时间,核对电价时段表 |
| 收益低于预期 | 充放电效率损耗大或峰谷价差小 | 分析实际充放电量,优化 SOC 上下限 |
5.2 结合示例代码做排查
示例程序本身也可以作为排查工具。当现场调度策略异常时,可以把真实电表采集的负荷、光伏、SOC 数据导入这个模拟框架,跑一遍策略逻辑,看结果是否符合预期。这样能快速区分“是策略问题还是执行层问题”。
在真实项目中,策略不上传下发通道和策略运算通道要分开。EMS 计算的功率值会先存储在中间变量,再通过通信服务下发到 PCS。日志中应该同时打印“计算功率”和“下发功率”,方便对比。
6. 最佳实践与工程建议
6.1 软件工程侧
首先,配置必须外部化。电价时段、电池容量、SOC 上下限、功率限制、效率系数等参数,全部放到配置文件或配置中心,不要硬编码在代码里。项目切换时,往往只需要调整参数,不需要重新编译。
其次,日志要完整。至少需要记录时间、SOC、充放电功率、并网点功率、电价、指令来源。这些日志不仅用于排查问题,也用于后续的收益计算和碳减排核算。
第三,异常处理要分层。通信异常、设备异常、策略异常要区分对待。比如 BMS 通信中断 5 秒内可以只告警,超过 30 秒则需要停机保护;通信恢复后需要重新读取一次完整的电池状态,再继续调度。
6.2 安全与合规侧
储能项目涉及高压、大电流和电池热失控风险,软件侧必须严格遵守“先保护、后收益”的原则。
- 任何情况下,SOC 不得超过电池厂商规定的安全范围。
- 电池温度、电流、电压异常时,调度程序必须立即退出,交由 BMS 或 PCS 保护逻辑处理。
- 防逆流逻辑要独立于收益策略,不能因为追求收益而关闭防逆流限制。
- 涉及远程控制时,必须增加权限校验和指令合法性校验,避免误操作导致安全事故。
6.3 海外项目落地要点
海外工商业储能项目在软件层面有几个明显不同点:
一是并网标准不同。北美市场通常需要满足 UL 9540、IEEE 1547 等标准,欧洲市场需要 CE 认证和当地电网导则。EMS 的并网保护逻辑需要根据当地标准调整。
二是通信协议更加多样。海外项目常用 Modbus TCP、SunSpec Modbus、IEC 61850 等协议,部分市场还要求支持 OpenADR 需求响应协议。对接前一定要先拿到对方的协议文档和调试工具。
三是电价机制更复杂。有些海外市场的实时电价每 15 分钟变化一次,有些需要参与电网辅助服务,这些都会直接影响 EMS 调度算法。
四是本地化运维。海外项目时差、语言、现场调试成本高,一定要在出厂前完成充分的工厂测试,并确保云平台支持远程运维和远程策略下发。
7. 总结与学习路线
这篇文章从工商业储能系统的架构出发,拆解了 EMS 的核心能力,并写了一个简化但可运行的调度程序。通过这个程序,你可以理解:
- 储能系统为什么需要 EMS,以及它和 BMS、PCS 的分工。
- 峰谷套利调度策略的基本逻辑。
- SOC 约束、功率约束、防逆流约束在代码中如何落地。
- 配置外部化对多项目复用的重要性。
如果你打算深入储能软件开发方向,下一步可以重点学习三个方向:
一是通信协议。Modbus TCP 是门槛最低的,建议用模拟器自己写一个 BMS 数据采集服务,把寄存器地址、字节序、系数转换都跑一遍。
二是预测算法。真实项目的负荷预测和光伏预测会直接决定调度效果,可以从线性回归、时间序列模型开始学习。
三是优化算法。当调度目标从“规则策略”升级到“收益最优”时,通常需要引入线性规划、动态规划或启发式算法,建议先以一天 24 个点、电池约束为条件,搭建一个简单的优化模型。
工商业储能软件是一个交叉领域,涉及电力、电池、通信、软件和数据分析。上手时不用追求大而全,从一个小模块做起,比如本文的调度决策函数,把它理解透,再逐步扩展,就能在项目里发挥实际价值。