从零理解EMS——从能量流到边缘控制器 · 第04篇
在配置页面把“允许送电功率”设为0,并不等于现场已经没有逆流。真正的问题是:多出来的电由谁消纳?设备是否做得到?命令写入之后,电表是否真的回到目标范围?
本文用光伏70kW、负载40kW的教学场景,把测量、约束、分配、执行和反馈连成一条完整的控制链。即使没有看过前几篇,也可以从本文的符号约定开始。
一、先拆掉一个误解:“不准送电”只是目标,不是动作
假设站内光伏正在发70kW,负载只用40kW,储能暂时不充不放。忽略损耗,剩余30kW会流向哪里?
如果系统与电网连接,而且没有其他消纳途径,这30kW就可能通过并网点送出去。
把配置改成“禁止送电”,并不会让这30kW自动消失。EMS必须让现场某些设备改变行为:
- 电池允许充电时,增加储能吸收功率;
- 已经在放电时,减少储能向交流系统的输出;
- 有可控光伏时,降低光伏实际出力;
- 有已获授权的柔性负载时,在能力范围内增加用电。
把它想成一盆水:“不许溢出”是一条规则,打开排水通道、暂存多余水量或关小进水阀,才是动作。
但这个比喻有边界:电网不是等待慢慢溢出的水盆,电气系统中的功率变化会立即反映到真实运行中。储能能否吸收、光伏多久响应,都有自己的限制。
图1:本例把富余30kW分成储能吸收20kW与光伏限发10kW。限发表示这部分电没有继续发出,不是新增了10kW负载。
二、把目标写成能检验的不等式
2.1 先统一四个功率的方向
本文统一使用交流侧有功功率,单位为kW:
| 变量 | 含义 | 符号约定 |
|---|---|---|
P_grid | 并网点与电网交换功率 | 正值取电,负值送电 |
P_pv | 光伏交流侧实际出力 | 发电记非负值 |
P_bat | 储能PCS交流侧实际功率 | 正值放电,负值充电 |
P_load | 本模型统计范围内的负载功率 | 用电记非负值 |
在同一计量边界、忽略损耗的教学模型中:
P_grid = P_load - P_pv - P_bat本例初始值为:
P_grid = 40 - 70 - 0 = -30kW也就是向电网送出30kW。
2.2 “零送电”和“限送电”
如果允许送电的最大幅值为E,且E >= 0,约束应写成:
P_grid >= -E禁止送电时,E=0,约束就是P_grid>=0;允许送电5kW时,则是P_grid>=-5。
因此,P_grid=-2kW在第二种场景中可能符合送电限值,但在第一种场景中不符合。负数不是自动代表故障,要先看当前约束。
2.3 为什么有时不跟踪0,而是保留取电余量?
在禁止送电的前提下,可以讨论以一个小的正取电值作为调节目标,为测量误差和短期波动留出空间。但余量不是越大越好:它会增加取电、减少自用或造成更多限发。
例如教学假设取电目标是2kW,则本例需使净发电再减少2kW。这个2kW仅用于算例,不是推荐产品参数,也不能保证所有负载变化下瞬时不逆流。
三、第一关:电表测得准,才有资格谈控制
防逆流关注的是并网连接边界,常用PCC指代这个位置。PCS支路功率、光伏功率、并网点净功率不是同一件事。
如果只读取PCS“正在充20kW”,并不能知道另外的光伏、负载变化是否已经使站点送电。
图2:值、方向、时间和品质要一起检查。通信正常,不代表测量数据一定在更新。
至少核对以下信息:
1.测量位置:确实覆盖需要控制的并网边界。
2.方向与单位:CT方向、点表正负号和W/kW转换正确。
3.品质与新鲜度:不是离线、无效值或超过有效期的旧值。
4.时间一致性:PCC、PV和PCS数据构成可用于推导的快照,而不是不同时间的数据拼接。
5.能力可信性:允许充电功率、可限发范围等限制同样没有过期。
零值不等于失效,稳定值不等于冻结。反过来,持续收到报文也不证明设备内部采样一直正常。需要根据源设备时间、更新计数、告警和其他测量进行判断。
还有一个盲点:如果负载功率是通过PCC、PV和PCS反算出来的,再用同一条公式“验证功率平衡”,结果为0只是数学自洽,不是第二套独立证据。
四、第二关:先看能力,再分配这30kW
本例进一步假设:
- 储能当前允许充电20kW;
- 光伏支持在当前出力以下限发;
- 无其他可调负载;
- 本次策略优先使用允许的储能充电能力,再限制PV;
- 不涉及离网切换、无功分配或硬件保护动作。
这只是一个清晰的分配顺序,不是所有项目的通用最优顺序。
4.1 储能先吸收20kW
储能充电记为负:
P_bat_target = -20kW如果PV仍发70kW,则预计:
P_grid = 40 - 70 - (-20) = -10kW还有10kW向外送,任务尚未完成。
4.2 光伏再减少10kW
将PV目标设为60kW:
P_grid = 40 - 60 - (-20) = 0kW静态上,这组动作满足目标:
| 对象 | 初始状态 | 本次目标 | 对减少送电的贡献 |
|---|---|---|---|
| 储能 | 0kW | -20kW,充电 | 吸收20kW |
| 光伏 | 70kW | 60kW实际出力目标 | 少发10kW |
| 并网点 | -30kW | 预计0kW | 总计改善30kW |
注意:“光伏当前可发70kW”“EMS设定60kW”“设备实际发出多少”是三个不同字段。如果设备内部还在爬坡,不能把60kW立即写进实际值数据库。
五、第三关:收到ACK之后,还需要一次真实回读
假设PV已经降到60kW,但PCS实际只充了15kW。
此时实际电表功率可能是:
P_grid_actual = 40 - 60 - (-15) = -5kW站点仍在送电5kW。
图3:第一次预测为0,实际仍送5;下一轮必须读取真实出力及更新后的能力,而不是复用上次目标冒充反馈。
接下来不能只有一个固定答案:
- 若储能仍允许充到20kW,只是响应较慢,应结合命令状态和响应期限判断,不能立刻把正常延迟误判成永久降额。
- 若确认当前最多只能充15kW,应更新能力。在负载与其他条件不变时,可进一步将PV限至55kW。
- 若电表或设备数据不可信,则不应继续拿旧数据算出一个看似精确的补偿量。
本例采用第二种分支:已确认可充上限为15kW,PV降至55kW后:
P_grid = 40 - 55 - (-15) = 0kW这说明完整闭环至少是:
可信快照 → 目标与约束 → 分配 → 下发 → 回执 → 实际回读 → 效果验证 → 下一轮ACK表示通信或设备接受了某个请求,VERIFIED才表示在约定的测量与验证条件下达到了效果。
六、第四关:响应时间不是CPU计算时间
负载可能在设备还没响应之前继续变化。因此,静态算对了,不代表动态过程没有短暂逆流。
完整链路通常包含:
测量更新等待 + 通道排队与传输 + 快照校验与计算 + 命令下发 + 设备动作与爬坡 + 反馈采集和验证实际并行任务可能重叠,应该根据时间线分析,而不是机械相加所有最坏值。
图4:假设无重叠的一次串行链路,测量200ms、采集排队与传输300ms、算法10ms、命令传输100ms、设备500ms、确认200ms,总计1310ms。不是实测结果或验收指标。
在这个教学时间线里,将算法从10ms压缩到1ms,只减少9ms,并不能让整条链路变成1ms。
同样,若设备功率变化率被限制为每秒10kW,需要增加30kW吸收功率时,单凭缩短EMS循环也不能消除设备爬坡时间。
所以,不能仅凭“采用高性能处理器”“云端每秒刷新”或“发送命令很快”,就宣称实现了任意条件下的瞬时零逆流。
七、零点附近为什么容易反复调节?
测量噪声、采样时间差、设备响应延迟和负载变化,都可能让功率在目标附近来回摆动。增加增益或无止境地加快下发,并不一定改善效果。
| 手段 | 主要用途 | 需要特别说明的边界 |
|---|---|---|
| 死区 | 小误差时减少无意义调节 | 如果死区覆盖送电侧,等于容忍一定反送,必须明确是否允许 |
| 滞回 | 进入和退出使用不同阈值 | 不能用一个通用阈值代替场景验证 |
| 爬坡限制 | 约束命令变化速度 | 可能与快速消除逆流的要求存在取舍 |
| 有限等待与回读 | 让实际响应进入判断 | 等待太久也可能错过关键变化 |
| 限幅与抗积分饱和 | 不让超出能力的需求持续累积 | 使用积分算法时才涉及相应积分管理 |
对于禁止送电的边界,常见设计思路是把工作点放在允许范围内,再设计误差与退出条件。但余量、死区、滞回和验证容差是不同概念,不能混成一个“防逆流精度”数字。
本文不为它们设置通用工程定值,也不声称下面的代数示例验证了动态稳定性。
八、第五关:能力不足和测量失效,必须成为正式输出
8.1 电池从“可充20kW”变成“禁止充电”
原工况为负载40、PV60、储能充20,PCC约为0。如果充电停止而PV和负载暂时不变,站点会出现约20kW富余。
EMS需要重新分配。例如PV可继续下调时,静态目标可以降至40kW。但禁止充电发生到限发真正生效之间的物理风险,不能靠一张静态计算表消除。
8.2 可调资源不够时,目标本来就不可达
仍以负载40、PV70、可充20为例。若受教学能力条件限制,PV当前最低只能到65kW,则最佳静态结果仍是:
P_grid = 40 - 65 - (-20) = -5kW这5kW是未能满足的约束缺口,不应隐藏。需要返回不可达状态、缺口和原因,并按批准方案处置。
8.3 电表失联,不等于当前功率为0
图5:数据失效先阻止基于该数据的新调节计算;这不意味着自动保持旧目标,也不等于已经进入电气安全状态。
| 异常 | 不能这样处理 | 应明确设计的内容 |
|---|---|---|
| PCC数据过期 | 把最后值或默认0继续当实时输入 | 测量有效期、后备观测、场景降级与恢复条件 |
| PCS失联 | 从软件列表删除后把物理出力也归零 | 旧目标可能仍在执行,实际贡献与可控能力需分开 |
| 指令超时 | 无限重复写,或断言没有执行 | 回读、有限重试、去重与不确定状态 |
| 云端断网 | 本地控制一起停,或无限保持远程目标 | 本地策略、目标期限和控制权交接 |
| 电池禁充 | 因为防逆流优先而强行充电 | 禁令生效,其他资源重算并暴露缺口 |
“暂停本轮计算”只描述软件行为。真实系统下一步限制谁、保留谁、是否需要独立联锁,要按并网/离网与关键负载场景评审。本文不提供一条适用于所有系统的“全部停机”指令。
九、写一个基于实际反馈的教学分配器
接下来把前面的思路写成一个可以运行的小程序。为控制范围明确,先固定假设:
- 单台储能、一个可限发PV,所有输入属于同一有效交流侧快照;
- 本次动作期间负载不变,忽略损耗;
- 只演示限制送电,不实现PV恢复、自用优化、SOC积分或无功控制;
- 教学策略不安排放电,必要时请求停止已有放电,但真实执行仍需确认;
charge_cap为已换算到交流侧且可信的当前允许充电幅值;
pv_floor为本轮模型允许PV达到的最低出力,不是通用设备参数。
9.1 用实际值计算这次应补多少
禁止送电时取target=0。允许送电时,目标可以取-export_limit;代码还允许向取电方向加一个非负裕量margin,所以:
target = -export_limit + margin correction = max(0, target - grid_actual)代码里的margin表示相对送电下边界向取电方向偏移的裕量。只有export_limit=0时,它才直接等于正取电目标。
如果PCS实际从bat_actual变为bat_command,在其他量暂时不变时:
grid_after_battery = grid_actual - (bat_command - bat_actual)剩余不足再由减少PV出力弥补。这样,上一轮命令没完全执行时,下一轮仍从真实回读出发,而不是把上次期望值当成已经完成的动作。
9.2 完整代码
将下列代码复制为anti_export_demo.py,用Python 3执行即可。只用标准库,不联网、不访问真实设备。
import math def plan_export(grid_actual, pv_actual, bat_actual, charge_cap, pv_floor=0.0, export_limit=0.0, margin=0.0, quality="GOOD", age_s=0.0, max_age_s=2.0): """依据同步交流侧快照计算教学目标,不控制真实设备。 功率入参单位均为kW;grid_actual正取负送,bat_actual正放负充。 charge_cap为允许充电幅值,pv_floor为本轮PV可达下限;均非负。 export_limit为允许送电幅值,margin为向取电方向偏移的裕量。 quality与age_s描述整组快照;max_age_s仅为教学新鲜度阈值。 返回命令、预计并网功率、目标缺口及规划状态;不代表执行成功。 数值非法抛ValueError;数据失效抛RuntimeError,由上层处理降级。 """ numbers = (grid_actual, pv_actual, bat_actual, charge_cap, pv_floor, export_limit, margin, age_s, max_age_s) if not all(math.isfinite(x) for x in numbers): raise ValueError("inputs must be finite") if min(pv_actual, charge_cap, pv_floor, export_limit, margin, age_s) < 0 or max_age_s <= 0: raise ValueError("invalid nonnegative limits or age") if pv_floor > pv_actual: raise ValueError("PV floor exceeds actual PV in this model") if quality != "GOOD" or age_s > max_age_s: raise RuntimeError("snapshot invalid; approved fallback required") target = -export_limit + margin correction = max(0.0, target - grid_actual) # 从实际功率扣除需纠正量,再裁剪到本模型允许的充电区间。 # 即使BMS可充上限骤降,也不能继续沿用原来更大的充电功率。 bat_command = max(-charge_cap, min(0.0, bat_actual - correction)) grid_after_battery = grid_actual - (bat_command - bat_actual) pv_reduction = max(0.0, target - grid_after_battery) pv_command = max(pv_floor, pv_actual - pv_reduction) predicted_grid = grid_after_battery + (pv_actual - pv_command) gap = max(0.0, target - predicted_grid) return { "bat_command": bat_command, "pv_command": pv_command, "predicted_grid": predicted_grid, "gap": gap, "status": "UNREACHABLE" if gap > 1e-9 else "PLANNED", } def run_checks(): """无入参;运行12个静态教学场景并打印结果,无返回值。""" # 8个正常/受限场景:初次分配、禁充、反馈修正、能力骤降、 # 允许送电、保守裕量、目标不可达、已有放电。 cases = [ ((-30, 70, 0, 20), {}, (-20, 60, 0, 0)), ((-30, 70, 0, 0), {}, (0, 40, 0, 0)), ((-5, 60, -15, 15), {}, (-15, 55, 0, 0)), ((0, 60, -20, 0), {}, (0, 40, 0, 0)), ((-30, 70, 0, 20), {"export_limit": 5}, (-20, 65, -5, 0)), ((-30, 70, 0, 20), {"margin": 2}, (-20, 58, 2, 0)), ((-30, 70, 0, 20), {"pv_floor": 65}, (-20, 65, -5, 5)), ((-40, 70, 10, 20), {}, (-20, 60, 0, 0)), ] keys = ("bat_command", "pv_command", "predicted_grid", "gap") for args, options, expected in cases: result = plan_export(*args, **options) for key, wanted in zip(keys, expected): assert math.isclose(result[key], wanted, abs_tol=1e-9) wanted_status = "UNREACHABLE" if expected[3] > 0 else "PLANNED" assert result["status"] == wanted_status # 4个异常场景:过期、品质不合格、NaN、负能力。 invalid = [ ((-30, 70, 0, 20), {"age_s": 3}, RuntimeError), ((-30, 70, 0, 20), {"quality": "BAD"}, RuntimeError), ((float("nan"), 70, 0, 20), {}, ValueError), ((-30, 70, 0, -1), {}, ValueError), ] for args, options, error_type in invalid: try: plan_export(*args, **options) except error_type: continue raise AssertionError("invalid input was not rejected") print("12 static scenarios passed") for label, args, options in [ ("initial", (-30, 70, 0, 20), {}), ("feedback", (-5, 60, -15, 15), {}), ("unreachable", (-30, 70, 0, 20), {"pv_floor": 65}), ]: r = plan_export(*args, **options) print(f"{label}: bat={r['bat_command']:.1f}, " f"pv={r['pv_command']:.1f}, grid={r['predicted_grid']:.1f}, " f"gap={r['gap']:.1f}, status={r['status']}") if __name__ == "__main__": run_checks()9.3 实际运行输出
本次编制时已执行上面的代码,12个静态场景检查通过,输出如下:
12 static scenarios passed initial: bat=-20.0, pv=60.0, grid=0.0, gap=0.0, status=PLANNED feedback: bat=-15.0, pv=55.0, grid=0.0, gap=0.0, status=PLANNED unreachable: bat=-20.0, pv=65.0, grid=-5.0, gap=5.0, status=UNREACHABLE输出中的PLANNED只表示“在本模型输入和假设下,预计达到目标下边界”,不是VERIFIED。如果原来已处于允许范围且没有额外调节,结果也可以是PLANNED,并不表示必须精确跟踪该目标。
本例还故意保留了UNREACHABLE:PV最低65kW且最多充20kW时,预计仍送5kW。相比简单返回success=True,明确缺口更方便上层处理。
9.4 这段程序没有证明什么?
它没有模拟串口排队、PCS爬坡、PV恢复、多机同步、SOC随时间变化、故障后的电气行为或保护系统。max_age_s=2.0也只是教学阈值。
实际系统不能在循环外简单捕获异常后“什么都不做”。代码抛出数据失效异常,是为了阻止使用坏数据计算新的目标;后备动作、旧目标处置和恢复授权要由独立的场景策略处理。
第二次feedback场景是手动给定的真实值假设,用来验证“更新快照和能力后重新算”的逻辑,不是实时硬件在环仿真,更不能作为零逆流性能证据。
十、如何验证:不要只截一张电表为0的图
一个可信的控制记录,应能回答:
什么时候发生了功率变化? 当时用了哪些测量、数据有多旧? 目标由谁提出,哪些约束生效? 各设备收到什么命令? 什么时候收到ACK? 实际功率如何变化? PCC是否在约定条件下满足要求? 没有满足时,系统怎样报告并处置?图6:命令、设备反馈和PCC反馈必须能关联到同一次调节。页面上的目标值不能代替实际测量。
建议至少记录以下几类信息:
| 类别 | 需要说明什么 |
|---|---|
| 测试条件 | 拓扑、并网边界、设备数量、波特率、初始SOC、动态能力与负载变化 |
| 时间定义 | 从哪个事件开始计时,到哪个可验证条件结束;区分算法耗时与端到端响应 |
| 功率效果 | 最大反送幅值、达到约定范围所需时间、稳态偏差及连续验证条件 |
| 数据质量 | 关键数据年龄、缺失、异常、时间一致性与测量精度 |
| 异常覆盖 | 电表过期、禁充、失联、命令拒绝、通信延迟和能力不足 |
| 证据关联 | command_id、请求、限后值、ACK、实际值、失败原因和配置版本 |
若要统计反送电能,可按实际测量计算max(-P_grid, 0)随时间的积分;允许部分送电时,还可以单独统计超出限值的部分。但这些指标采用什么容差、窗口和判据,必须在需求与验收方案中明确。
一张“当前P_grid=0”的截图,既无法说明刚才是否出现过反送,也无法证明下一个负载突变时还会合格。
十一、三个检查题,把控制链再走一遍
问题A:PV70、负载40,储能从可充20kW变成禁止充电。假设其他条件允许,静态上PV应降到多少?
答案:降到40kW,储能目标为0,预计PCC为0。动态过程是否满足要求,还取决于实际响应和系统保护边界。
问题B:PV已经降至60kW,储能目标为充20kW,但实际只充15kW,负载仍为40kW。收到ACK能否直接判成功?
答案:不能,实际PCC仍约为40-60-(-15)=-5kW。需要检查设备响应期限及真实能力,选择等待、继续调节或报告异常。
问题C:并网电表最后一次回读为0,随后通信断开。继续把0显示成实时值并维持闭环,有什么问题?
答案:失去了当前观测却伪装成正常状态。应标记过期并触发批准的场景处理,而不是用默认值替代真实测量。当前设备是否还在执行旧目标也需要纳入判断。
十二、总结:防逆流是一条能解释、能验证的控制链
从30kW富余功率出发,我们先用了20kW允许的储能充电能力,再限发10kW;当实际充电只有15kW时,再通过真实反馈更新能力和PV目标。
重点不在于这几个数字,而在于每一步都能解释:
- 测得对:并网点位置、符号、时间和品质都明确。
- 算得可行:目标服从BMS、PCS和可调资源约束。
- 发得有序:同一设备有明确控制权,指令可追踪。
- 验得真实:使用实际反馈,而不是目标值或ACK。
- 失败不隐藏:数据失效和目标不可达都有正式处理分支。
“把目标设为零”只是起点;知道30kW去了哪里,以及如何证明它真的去了那里,才是完整的EMS控制思路。
下一篇将进一步讨论:一张电表数据为什么不能直接拿来控制?从测量位置、CT方向、时间戳与品质标记入手,建立可信的输入链路。
工程边界说明:本文基于通用概念和原创教学案例,所有功率、延迟、能力及阈值均为示例,不构成并网、保护定值、瞬时零逆流或实机性能承诺。代码不连接真实设备,配图不用于现场接线。真实实施必须结合设备协议、测量条件、动态能力和批准的安全方案验证。