简介:这份资源是面向电力工程、新能源与自动化方向学习者及开发者的微电网能量管理系统(MGEMS)Python实现方案,适合具备一定Python基础、希望理解分布式能源调度逻辑的中高级读者。压缩包共555个文件,约3.08MB,以85个py源码与103个pyc编译文件为核心,辅以94个html、140个js、41个css构建前端监控界面,另有po/mo多语言文件、png/jpg图标及xml配置等,整体结构接近可运行的完整项目。内容围绕实时监控、能源调度、电力平衡、故障隔离与优化运行展开,涵盖pandas/numpy数据处理、scikit-learn负荷与可再生能源出力预测、scipy.optimize经济调度求解、控制策略编程及matplotlib/tkinter可视化界面等模块,可帮助读者理解从数据采集、预测建模到调度指令生成与设备控制的完整链路。目前已有858人学习,适合作为课程设计、科研原型或微电网算法验证的参考工程。
1. 微电网能量管理系统用 Python 落地:从设备接入到调度决策的一条完整链路
光伏板在中午满发,储能电池 SOC 还停在 45%,负荷侧只有办公楼的基础照明在跑,逆变器却因为反送电超限被限功率——这是很多小型微电网项目在并网调试阶段都会撞上的场景。微电网能量管理系统(EMS)要干的事,就是在这几秒钟里判断:是给电池充电、还是切掉部分光伏、还是启动可调负荷,让整个系统既不越限也不浪费。Python 在这个位置上不是用来做界面演示的,而是承担数据采集、状态估计、优化调度、指令下发这条主链路。它适合两类人:一类是做新能源或电力电子的工程师,手上有设备但缺一套能跑起来的调度逻辑;另一类是 Python 开发者,想切入能源赛道但不知道从哪一层写起。下面按我实际搭过的一套结构,把设备接入、数据建模、调度求解、指令执行和排错逐个拆开。
2. 微电网 EMS 的 Python 技术栈选型:为什么不是组态软件加脚本
2.1 从设备层到决策层的四段式结构
一套能长期跑的微电网 EMS,在 Python 侧通常拆成四段。第一段是采集层,负责从 Modbus TCP、IEC 104 或 MQTT 拿设备实时数据,常见做法是用 pymodbus 或 paho-mqtt 起独立进程,把原始点表写进 Redis 或本地时序库。第二段是模型层,把光伏、储能、负荷、并网点的物理量统一成带时间戳的对象,这一步决定了后面优化问题能不能写干净。第三段是调度层,用线性规划或规则引擎算出未来 15 分钟到 24 小时的功率计划。第四段是执行层,把计划翻译成逆变器和 PCS 能识别的寄存器写值或遥调指令。
这四段之间不要用函数直接调用串起来,而是用消息队列或共享内存解耦。我见过太多项目把采集和调度写在一个 while 循环里,结果 Modbus 超时一次,整个调度周期就跟着卡死。用 Python 的 asyncio 或独立进程加 Redis 队列,采集归采集,调度归调度,单点故障不会把整条链路拖垮。
2.2 为什么选 Python 而不是组态软件内置脚本
组态软件做监控画面和报警很快,但它的脚本环境通常不支持 scipy、pulp、cvxpy 这类优化库,也不方便做版本管理和单元测试。微电网的调度逻辑一旦从固定规则升级到经济优化,就需要频繁改目标函数和约束条件,这时候 Python 的表达能力优势很明显。另一个现实原因是数据侧:很多项目要把历史运行数据拉出来做分析,Python 的 pandas 和 matplotlib 能直接出图,不用再导一次数据。
但 Python 不是万能的。毫秒级的保护逻辑不要放在 Python 里,那是继电保护装置和 PCS 控制器的事。EMS 的调度周期一般在秒级到分钟级,Python 完全够用。选型时把边界划清楚:Python 管策略和协调,底层设备管快速闭环。
2.3 最小可运行环境与依赖安装
先确认 Python 版本。微电网 EMS 涉及数值计算和通信,建议用 3.8 以上版本,3.8 到 3.11 都验证过。安装时勾选 Add Python to PATH,装完在命令行确认:
python --version pip --version如果机器上同时有多个 Python,用python -m pip而不是直接pip,避免装到错误的解释器里。这是新手最容易翻车的地方,明明装了库,运行时报 ModuleNotFoundError,就是因为 pip 和 python 指向了不同环境。
核心依赖分三类。通信类:pymodbus、paho-mqtt。计算类:numpy、pandas、scipy。优化类:pulp 或 cvxpy。可视化调试用 matplotlib。安装命令:
python -m pip install pymodbus paho-mqtt numpy pandas scipy pulp matplotlib如果项目要连 Oracle 或 MySQL 存历史数据,再加 cx_Oracle 或 pymysql。不要一次装太多,先把最小链路跑通再扩。
2.4 用数据类定义设备模型
设备模型不要用裸字典,字段名写错要到运行时才发现。用 dataclass 把光伏、储能、负荷、并网点各定义一个类,类型和单位写在字段上:
from dataclasses import dataclass, field from datetime import datetime @dataclass class PV: power_kw: float = 0.0 # 当前有功功率,单位 kW available_kw: float = 0.0 # 当前可发最大功率,受辐照和温度影响 curtailed_kw: float = 0.0 # 被限掉的功率 ts: datetime = field(default_factory=datetime.now) @dataclass class Battery: soc: float = 0.5 # 荷电状态 0~1 power_kw: float = 0.0 # 正为充电,负为放电 max_charge_kw: float = 50.0 max_discharge_kw: float = 50.0 capacity_kwh: float = 200.0 ts: datetime = field(default_factory=datetime.now) @dataclass class Load: power_kw: float = 0.0 forecast_kw: float = 0.0 ts: datetime = field(default_factory=datetime.now)字段单位统一用 kW 和 kWh,不要混用 W 和 MW。时间戳统一用本地时间还是 UTC 要在项目初期定死,后面接数据库和前端时改起来很痛苦。soc 用 0 到 1 的小数,不要用百分数,避免计算时反复乘除 100。
3. 用 Python 跑通微电网调度最小闭环:采集、建模、求解、下发
3.1 Modbus TCP 采集储能 PCS 实时数据
储能 PCS 一般支持 Modbus TCP,寄存器地址表由厂家提供。下面是一个最小采集函数,读 SOC、有功功率和运行状态:
from pymodbus.client import ModbusTcpClient def read_pcs(ip="192.168.1.10", port=502, unit=1): client = ModbusTcpClient(ip, port=port, timeout=2) if not client.connect(): raise ConnectionError(f"无法连接 PCS {ip}:{port}") try: # 寄存器地址按厂家手册改,这里只是示例 rr = client.read_holding_registers(address=1000, count=3, slave=unit) if rr.isError(): raise IOError(f"读取失败: {rr}") soc_raw, power_raw, status_raw = rr.registers soc = soc_raw / 1000.0 # 假设缩放系数 1000 power_kw = (power_raw - 30000) / 10.0 # 偏移量按手册改 return {"soc": soc, "power_kw": power_kw, "status": status_raw} finally: client.close()逻辑说明:每次采集新建连接再关闭,适合低频轮询。如果采集周期小于 1 秒,应该复用连接,否则 TCP 握手开销会拖慢整体节奏。参数上,timeout 不要设太大,2 秒足够,超时后让上层重试而不是死等。slave 和 unit 在不同库版本里叫法不同,pymodbus 3.x 用 slave,2.x 用 unit,升级库时要注意。
寄存器缩放系数和偏移量必须按厂家手册来,不要凭经验猜。我踩过的坑是某品牌 PCS 的功率寄存器用补码表示放电,直接按无符号读出来是 6 万多,调度器以为电池在疯狂充电,实际在放电。读原始值后先打印出来和 PCS 屏幕对照,确认无误再进模型。
3.2 把实时数据写进统一状态对象
采集回来的数据要汇总成一个系统状态,供调度器使用。用一个函数把各设备读数拼起来:
def build_system_state(pv, battery, load, grid_limit_kw=100.0): state = { "pv_kw": pv.power_kw, "pv_available_kw": pv.available_kw, "battery_soc": battery.soc, "battery_power_kw": battery.power_kw, "load_kw": load.power_kw, "grid_limit_kw": grid_limit_kw, "net_kw": pv.power_kw - load.power_kw - battery.power_kw, } return statenet_kw 为正表示往电网送电,为负表示从电网取电。并网点限值通常正反两个方向都有,这里只用一个 grid_limit_kw 简化,实际项目要拆成 import_limit 和 export_limit。状态对象每次调度周期重建一次,不要缓存旧值,否则设备通信中断时会拿过期数据做决策。
3.3 用 PuLP 写一个 15 分钟滚动优化
调度目标可以很简单:在并网点不越限的前提下,让光伏弃光最少、电池充放电成本最低。下面是一个单步优化示例,决策变量是电池功率和光伏限功率:
import pulp def dispatch_step(state, price_buy=0.6, price_sell=0.3): prob = pulp.LpProblem("ems_step", pulp.LpMinimize) p_bat = pulp.LpVariable("p_bat", -50, 50) # 正充负放 p_curtail = pulp.LpVariable("p_curtail", 0, state["pv_available_kw"]) p_grid = state["pv_available_kw"] - p_curtail - state["load_kw"] - p_bat # 目标:购电成本 + 弃光惩罚 - 售电收益 prob += (price_buy * pulp.lpSum([p_grid]) * 0.25 + 10 * p_curtail * 0.25 - price_sell * pulp.lpSum([-p_grid]) * 0.25) # 并网点限值 prob += p_grid <= state["grid_limit_kw"] prob += p_grid >= -state["grid_limit_kw"] # SOC 约束,简化处理:充电不超过可充空间,放电不超过可用电量 prob += p_bat <= (1 - state["battery_soc"]) * 200 / 0.25 prob += p_bat >= -state["battery_soc"] * 200 / 0.25 prob.solve(pulp.PULP_CBC_CMD(msg=0)) return { "p_bat": pulp.value(p_bat), "p_curtail": pulp.value(p_curtail), "p_grid": pulp.value(p_grid), "status": pulp.LpStatus[prob.status], }逻辑说明:目标函数里购电成本按 0.25 小时折算,弃光惩罚系数 10 是拍脑袋值,实际项目要按上网电价和考核规则调。SOC 约束这里用容量 200 kWh 和时间步长 0.25 小时换算成功率边界,简化了充放电效率,真实项目要乘效率系数。求解器用 CBC,PuLP 自带,不用额外装。如果求解状态不是 Optimal,不要直接下发指令,先记录状态并保持上一周期计划。
参数上,price_buy 和 price_sell 应该来自分时电价表,不要写死。电池充放电效率、SOC 上下限、最大充放电功率都要从配置文件读,方便现场调整。这个模型没有考虑电池循环寿命成本,如果项目对电池衰减敏感,要在目标里加一项充放电功率的绝对值惩罚。
3.4 把调度结果翻译成 PCS 写寄存器指令
优化出来的 p_bat 要下发给 PCS。写寄存器和读一样,注意缩放和符号:
def write_pcs_power(p_bat_kw, ip="192.168.1.10", port=502, unit=1): client = ModbusTcpClient(ip, port=port, timeout=2) if not client.connect(): raise ConnectionError("PCS 连接失败,指令未下发") try: # 假设功率寄存器缩放 10 倍,偏移 30000,充电为正 raw = int(p_bat_kw * 10 + 30000) raw = max(0, min(65535, raw)) wr = client.write_register(address=2000, value=raw, slave=unit) if wr.isError(): raise IOError(f"写寄存器失败: {wr}") return True finally: client.close()下发前要做范围校验,p_bat_kw 超过 PCS 最大功率时截断并记录告警。写寄存器后建议回读一次确认,有些 PCS 会拒绝越限指令但不报错。如果项目要求高可靠性,指令下发和回读确认要放在同一个事务里,失败则重试或回退到安全模式。
4. 微电网 EMS 避坑与排查:那些让调度器发疯的现场问题
4.1 现象:SOC 显示 100% 但电池还在充电
原因通常是 SOC 寄存器缩放系数搞错,或者 PCS 上报的是健康度 SOH 而不是 SOC。解决方法是拿万用表和 PCS 屏幕对照,确认寄存器地址和缩放。另一个可能是 BMS 和 PCS 的 SOC 不一致,EMS 要明确以哪个为准,一般以 BMS 为准。
4.2 现象:优化求解偶尔返回 Infeasible
原因多是约束之间打架,比如并网点限值很窄,同时电池 SOC 已经到边界,光伏又必须满发。解决方法是给约束加松弛变量,或者在求解失败时降级到规则调度:优先保证并网点不越限,其次弃光,最后动电池。不要因为求解失败就让系统停摆。
4.3 现象:Modbus 读到的功率值跳变剧烈
原因是采集周期太短,读到了寄存器更新的中间态,或者没有做滤波。解决方法是在采集层加滑动平均,窗口 3 到 5 个点。但滤波会引入延迟,调度周期短的场景要权衡。另一个可能是通信干扰,检查网线和接地。
4.4 现象:调度指令下发后 PCS 不执行
先确认写寄存器返回是否成功,再确认 PCS 是否处于远程控制模式。很多 PCS 默认本地模式,接受 Modbus 写值但不执行。还要检查急停或消防信号是否触发,这些信号会闭锁远程指令。排查时按「通信通不通、模式对不对、闭锁有没有」三步走。
4.5 现象:历史数据存进去时间戳乱序
原因是采集进程和存储进程用了不同时区,或者数据库字段类型不对。解决方法是全链路统一用 UTC 时间戳存储,展示时再转本地时区。数据库字段用 datetime 或 timestamp,不要用字符串。
5. 让调度策略可验证:回放历史数据与影子模式
5.1 用历史数据回放验证调度逻辑
现场调试最怕改一行代码就要等一天看效果。我一般会先把历史运行数据导出成 CSV,用 pandas 读进来,按时间戳逐点喂给调度函数,把决策结果和实际运行对比。这样能在办公室把大部分逻辑错误找出来。
import pandas as pd def replay(csv_path): df = pd.read_csv(csv_path, parse_dates=["ts"]) results = [] for _, row in df.iterrows(): state = { "pv_available_kw": row["pv_available_kw"], "load_kw": row["load_kw"], "battery_soc": row["soc"], "grid_limit_kw": 100.0, } out = dispatch_step(state) results.append({**row.to_dict(), **out}) return pd.DataFrame(results)回放时重点关注三类点:求解失败的时段、电池功率频繁正负切换的时段、并网点接近限值的时段。这些点往往对应约束设置不合理或参数需要调整。回放通过后再上现场,能省掉大量来回。
5.2 影子模式:新策略先看不说
现场不允许随便改策略时,用影子模式。EMS 同时跑旧策略和新策略,新策略只计算不下发,把结果写进日志。运行几天后对比两套策略的购电成本、弃光量和电池循环次数。确认新策略更优且没有异常后再切换。这个做法在已经投运的站上特别有用,不用停机就能验证。
5.3 几个我固定会加的监控点
不管项目大小,这几个量我每次都会记录并画出来:并网点功率与限值的差值、电池 SOC 曲线、光伏弃光功率、调度求解耗时、通信失败次数。前三个看策略效果,后两个看系统健康度。求解耗时突然变长通常意味着约束变复杂或数据异常,通信失败次数上升要查网络或设备。
5.4 参数配置不要写死在代码里
电价、SOC 上下限、最大充放电功率、并网点限值这些现场会变的量,全部放 YAML 或 JSON 配置文件。代码里只留默认值兜底。我习惯用 YAML,可读性好,改完不用重启进程,加个文件监听重新加载就行。这样现场调试时不用改代码,减少引入新 bug 的机会。
最后说个习惯:每次改完调度逻辑,先跑回放,再跑影子模式,最后才切主控。直接上现场改策略,翻车概率太高。微电网 EMS 的代码量不大,但和物理设备绑得紧,慢一点比快一点稳。希望帮到你。
本文还有配套的精品资源,点击获取