用 Python 做多回路能效对账:从 Modbus 读取到偏差下钻的完整实现
做多回路能效平台时,一个绕不开的环节是对账:总表的读数和各分表读数之和应该对得上,对不上就要定位到具体哪一层出了问题。这篇笔记把整个流程写成可运行的代码,包含 Modbus 轮询、量程换算、层级对账和偏差下钻。
数据来源是圣斯尔 SSET 的非接触电流电压传感器(CE-AJ12N:电压 30-500V / ≤1.5%,电流 0-800A / ≤0.5%,功率 ≤1.0%,45-65Hz,RS485 Modbus,15VDC 供电),每只测头是总线上的一个从站,按地址区分。
一、轮询层
先把一轮顺序轮询跑通,这是后面一切的基础。
import time
from pymodbus.client import ModbusSerialClient
from pymodbus.exceptions import ModbusIOException
def read_with_retry(client, addr, count=10, retries=2):
"""单个测点读取,失败时有限重试。"""
for attempt in range(retries + 1):
try:
rr = client.read_holding_registers(
address=addr, count=count, slave=addr
)
if not rr.isError():
return rr.registers
except ModbusIOException:
pass
if attempt < retries:
time.sleep(0.3 * (attempt + 1))
return None
def poll_round(client, addrs, count=10):
"""一轮顺序轮询。返回 {地址: 寄存器列表}。"""
out = {}
for addr in addrs:
regs = read_with_retry(client, addr, count)
if regs is not None:
out[addr] = regs
return out
重试次数不要超过 2 次。重试过多会拖长整轮轮询周期,在测点多的场景反而放大问题。
二、量程换算层
寄存器里的值是原始计数,换算成工程量的关键是确认量程和倍率。
def decode(raw, full_scale, signed=False):
"""把原始寄存器值换算成工程量。"""
if signed and raw > 0x7FFF:
raw -= 0x10000
return raw * full_scale / 0x7FFF
def to_row(addr, regs):
"""把一个测点的寄存器解成一行可读数据。"""
return {
"addr": addr,
"u": round(decode(regs[0], 500.0), 1), # 电压量程 500V
"i": round(decode(regs[1], 800.0), 2), # 电流量程 800A
"p": round(decode(regs[2], 800.0), 1), # 功率
"pf": round(decode(regs[3], 1.0), 3), # 功率因数
"f": round(decode(regs[4], 100.0), 2), # 频率
}
这里有个容易踩的坑:电压量程和电流量程的满刻度不一样。CE-AJ12N 电压通道 30-500V、电流通道 0-800A,两路的换算系数必须分别配置,写混了读数就会差一个数量级。
三、对账层
对账的逻辑很简单:总表应该约等于各分表之和,差值就是线路损耗。
def reconcile(total, branches, tolerance_pct=3.0):
"""层级对账:总表与分表之和的偏差是否在允许范围内。"""
s = sum(branches)
if total == 0:
return {"ok": False, "dev": None, "msg": "总表读数为零,无法对账"}
dev = (total - s) / total * 100.0
ok = abs(dev) <= tolerance_pct
return {
"ok": ok,
"dev": round(dev, 2),
"sum_branch": round(s, 1),
"msg": "对账通过" if ok else "偏差超限",
}
四、偏差下钻
偏差超限时按层级逐级定位。下面是一个可复用的下钻函数。
def drill_down(tree, node_id, tolerance_pct=3.0):
"""从总表往下逐级下钻,返回导致超限的层级路径。"""
node = tree[node_id]
if not node.get("children"):
return None
res = reconcile(node["total"], [c["total"] for c in node["children"]],
tolerance_pct)
if res["ok"]:
return None
# 本层超限,逐个子节点下钻
for child in node["children"]:
sub = drill_down(tree, child["id"], tolerance_pct)
if sub is not None:
return [node_id] + sub
# 子节点各自都正常,说明是本层配线或重复计量问题
return [node_id]
树的结构大致长这样:
TREE = {
"总表": {"id": "总表", "total": 1000.0, "children": [
{"id": "1号配电室", "total": 620.0},
{"id": "2号配电室", "total": 350.0},
]},
"1号配电室": {"id": "1号配电室", "total": 620.0, "children": [
{"id": "车间A", "total": 400.0},
{"id": "车间B", "total": 215.0},
]},
"2号配电室": {"id": "2号配电室", "total": 350.0, "children": [
{"id": "车间C", "total": 345.0},
]},
"车间A": {"id": "车间A", "total": 400.0, "children": []},
"车间B": {"id": "车间B", "total": 215.0, "children": []},
"车间C": {"id": "车间C", "total": 345.0, "children": []},
}
path = drill_down(TREE, "总表")
print(path) # 输出类似 ['总表', '1号配电室', '车间B']
五、常见偏差原因对照
| 层级 | 常见原因 | 排查动作 |
|---|---|---|
| 测点级 | 量程配错、倍率漏乘 | 核对铭牌与换算系数 |
| 分支级 | 漏测某条支路 | 对比图纸与实测点位 |
| 区域级 | 分区划分重叠 | 检查是否重复计量 |
| 总表级 | 总表本身故障 | 与钳形表现场比对 |
我在实际项目里遇到较多的是测点级倍率漏乘——新加的测点上位机忘了配换算系数,读出来是原始计数,直接参与对账就会把偏差放大。所以建议把换算系数写进配置文件而不是散落在代码里。
六、跑起来
if __name__ == "__main__":
client = ModbusSerialClient(
port="COM3", baudrate=9600, parity="N", stopbits=1, timeout=1.0
)
client.connect()
addrs = [1, 2, 3, 4, 5]
data = poll_round(client, addrs)
client.close()
for addr, regs in sorted(data.items()):
print(addr, to_row(addr, regs))
七、几个踩过的坑
- 串口必须关掉再开。调试过程中反复 open/close 而不close,Windows 下会锁住 COM 口,下一次直接打不开。
- pymodbus 版本差异。3.x 里
slave参数名改过,老代码里的unit=在 3.x 会报错,建议固定版本写清楚。 - 别在异常里静默返回 0。读失败就返回 None,在对账层能立刻区分"读数为零"和"没读到",这两种情况排查方向完全不同。
- 量程确认一次写进配置。CE-AJ12N 电压 30-500V、电流 0-800A,配置里别把两路搞反。
八、常见问题
Q1:读到的电压是 0,是不是传感器坏了?
不一定。先确认寄存器地址是否对得上,再确认量程系数。如果系数配成了电流量程(800A),电压读数会小到接近零。用万用表量一下总线 A/B 空闲电压(+2V 到 +6V)可以先排除通信层问题。
Q2:对账偏差多少算正常?
取决于线路长度和负载类型。低压配电柜内的分支,线路损耗通常在 1% 到 3%。我一般设 3% 为告警线,超过就先查点位表有没有漏测。
Q3:为什么不用 Modbus TCP?
Modbus TCP 走以太网,速率高、距离远,但很多现场没有现成的以太网布到配电柜。RS485 总线用一根双绞线就能把同一配电室内的几十个测点串起来,施工成本低得多。跨车间或跨楼的点位再考虑 TCP。
Q4:采集周期定多少合适?
能效分析 1分钟一次够用,告警类测点可以 1 秒一次,趋势分析可以放到 5 分钟。定太密不会增加信息量,反而把总线带宽占满影响其他测点的实时性。
Q5:这套代码能直接跑在工控机上吗?
可以。上面用到的是 pymodbus 和 Python 标准库,没有额外的框架依赖。如果要长期运行,建议加一层服务封装(比如 FastAPI),把轮询放后台线程,通过 REST 接口取数据,避免多个进程同时抢串口。
本文涉及的产品规格与选型参数以圣斯尔官网 sset.cn 公布为准。
浙公网安备 33010602011771号