设备科半夜接到电话,CT室市电闪断,UPS顶上去了,可三分钟后电池电量掉到15%,还没等值班工程师赶到现场,设备已经因为电量耗尽强制关机。片子没出完,患者多等了两小时,科室主任的脸色比报告单还难看。事后复盘,问题根本不在UPS本身,而在“没人知道这台UPS的电池还能撑多久”。
后来我基于NUT(Network UPS Tools)这套开源能源监测系统,加上自写的采集程序和告警链路,做了一套面向医疗设备供电场景的UPS实时监测系统,源码和配置都整理成了开源项目挂在Gitee上。这篇文章把完整的设计思路、硬件选型、软件配置、部署记录和踩坑经验一次说清楚,给同样被断电焦虑折磨的设备科同行一份可以直接参考的作业。
1. 问题拆解:医疗供电场景到底在怕什么
1.1 医疗设备对供电的敏感点
医院的供电线路并不是想象中“一马平川”的稳定电网。雷电、市政施工、变压器检修,甚至同一栋楼里某台大功率空调压缩机启动瞬间拉低电压,都会在市电波形上留下痕迹。对CT、MRI、DSA这类大型影像设备,一次毫秒级的电压跌落就可能让球管曝光中断、扫描被迫重来。对ICU的呼吸机、监护仪、输液泵这些连续工作设备,断电带来的风险不只是设备损坏,更是治疗过程的中断——这种中断在某些场景下是分秒必争的。
UPS存在的意义,就是填补市电故障到备用发电机组启动之间那段真空期。我这些年巡检过不少科室,发现一个普遍问题:UPS安装完就没人管,电池鼓包、内阻增大、容量衰减、整机处于旁路状态,这些隐患平时完全看不出来,一旦真断电就全部暴露。至少有三分之一的在用UPS没有任何远程监控手段,电池状态对设备科来说就是个盲区。
1.2 从“有UPS”到“管好UPS”的需求拆解
动手做这套系统之前,我先把需求列了个清单。不追求大而全,只解决四个核心问题:
- 实时电量:电池剩余电量百分比、预计后备时间、当前负载率,这些关键参数必须随时可见。
- 可预警:剩余时间低于阈值时必须有人知道,而不是等设备黑屏后才被电话吵醒。
- 可追溯:每一次断电事件的时间、时长、电池表现都要有日志,方便事后复盘和电池健康评估。
- 可联动:对非生命支持类的服务器、存储设备,要在安全关机窗口内做自动联动关机,避免突然掉电损坏数据。
这四个需求直接决定了技术选型的方向:必须开源,必须支持多种UPS通信协议,必须方便二次开发对接。闭源商业监控软件我也看过,价格倒不是最大问题,真正的问题是医院设备科经常有定制化需求,闭源方案改不动,后期维护很被动。
2. 开源方案选型:为什么是NUT
2.1 三种监测通道的对比
很多同行一上来就想自己写Modbus协议解析,结果被各家UPS厂家的私有协议劝退。其实UPS对外暴露的监测通道主要有三类,选对了能省一半功夫。
- USB/HID通道:现代中小型UPS最常见的接口。插上USB线,系统识别成HID电源设备,通过usbhid-ups驱动就能读取状态。优点是免费、即插即用,绝大多数在线式UPS都支持。
- SNMP/网口通道:大型UPS或机架式UPS常配网络管理卡。通过SNMP的OID可以读到输入输出电压、负载率、电池容量、剩余时间等几乎全部参数。好处是纯网络化,不用近距离插线,但网络管理卡通常是选配件,价格不便宜。
- RS485/Modbus通道:工业级或老旧型号UPS常用,需要自己向厂家要协议文档,数据最原始,但解析工作量大。
我的判断是:中小型设备用USB通道,大型设备用SNMP通道,Modbus留给实在没有前两种接口的情况。这套系统在NUT层面把三种通道统一抽象成一套标准输出,上层应用根本不用关心底层是哪种协议。
2.2 NUT的核心价值与工作方式
NUT是Linux生态里最成熟的开源UPS管理套件,核心组件就三个:
- upsd:守护进程,维护UPS状态数据库;
- 各类驱动(如usbhid-ups、snmp-ups):负责跟具体UPS硬件通信;
- upsmon:监控进程,执行告警事件和关机策略。
它的设计把“采集”和“策略”完全解耦。命令行下一条upsc就能随时抓取任意监控点的参数,格式统一、简单直接。典型的输出是这样的:
$ upsc ct-ups@localhost battery.charge: 100 battery.runtime: 2400 battery.voltage: 27.4 input.voltage: 220.3 output.voltage: 219.8 ups.status: OL字段都是标准化的,battery.charge是剩余电量百分比,battery.runtime是预计后备秒数,ups.status为OL表示在线,OB表示正在电池供电。这套标准化字段就是整个项目的“数据底座”,后面的采集程序、告警逻辑全部基于它开发。
我当时敲定用NUT还有一个现实原因:它支持几百种UPS型号,社区驱动维护非常活跃,冷门型号大概率也能找到现成驱动。让我自己从头写一个UPS的USB HID协议解析,那工程量不是一般人能扛下来的。
3. 系统架构与核心实现细节
3.1 整体架构设计
整个系统我分成四层:
- 数据采集层:各科室的UPS通过USB或SNMP接入本地“探针盒”;
- 汇聚层:探针盒运行NUT驱动,把状态数据汇总到中心upsd服务器;
- 应用层:一套Python采集服务定时轮询upsc并写入MySQL数据库,同时把数据推给Grafana和告警模块;
- 执行层:upsmon根据阈值触发安全关机脚本,或经MQTT把消息推送到值班人员的手机。
探针盒我建议用树莓派或二手瘦客户机,配置要求不高。但有一个容易被忽视的细节:探针盒本身也必须接在UPS供电回路上。监测系统自己要先断电了,那所有告警都是空话。这个坑我踩过一次,那次是设备间里临时插线,把树莓派插在了普通墙插上,结果市电一跳,整个监控瞬间失联。
3.2 核心配置与参数说明
以一台ICU的3kVA在线式UPS为例,它带USB口,也带一块SNMP接口卡。我选择走USB通道,理由是这种规模没必要占用网络管理卡,USB通道足够稳定。
树莓派上安装NUT:
sudo apt install nut nut-client nut-server然后需要配置三个文件。
ups.conf,告诉NUT硬件在哪、用什么驱动:
[icu-ups] driver = usbhid-ups port = auto desc = "ICU Power"upsd.conf,配置监听地址和访问权限:
LISTEN 127.0.0.1 3493 LISTEN 192.168.10.20 3493upsmon.conf,配置主从关系和关机策略:
MONITOR icu-ups@localhost 1 admin secret primary POLLFREQ 5 SHUTDOWNCMD "/sbin/shutdown -h +0"这里有个非常重要的经验:MONITOR行最后的primary表示这台机器是主监控节点,它负责在电池耗尽前执行关机。医疗场景下,我强烈建议把upsmon的自动关机决策只用于服务器和存储设备,绝对不能用于生命支持类设备。让机器自动切断一台正在运行的呼吸机电源,这个责任没人敢背。正确的做法是保证告警到达人,由人做最终判断。这也是整个系统设计上和普通机房监控最大的区别。
3.3 电量与后备时间的二次估算逻辑
NUT上报的battery.runtime是UPS固件估算的,准确度的问题后面专门讲。我自己的做法是在应用层做了一次“二次估算”,目的是交叉验证,防止被单一数据源误导。
核心思路是:后备时间等于电池当前可用容量除以当前负载功率。可用容量从battery.charge百分比乘上电池组额定Wh得到,负载功率用输出电压和输出电流实时计算,也可以用UPS提供的outlet功率字段。每5秒算一次,得到滑动平均值:
runtime_est = battery_charge_pct * battery_rated_wh / load_power_w这个公式看着简单,实际很能说明问题。NUT固件估算的是厂商基于新鲜电池的理想值,而二次估算用的是实时负载和实时容量,两者差值一旦超过30%,基本可以断定电池容量已经明显衰减,该安排放电测试或者准备换电池了。这套交叉验证机制帮我们提前发现过两台电池容量衰减超过40%的UPS,都是在还没发生真实断电前处理掉的。
3.4 三级告警链路与联动设计
告警我设计了三级,每级触发条件和动作都不同:
- 提示级:市电异常,UPS进入电池供电。只推送通知,让值班人员知道当前状态。
- 预警级:剩余后备时间低于10分钟。推送值班人员,同时点亮声光报警器。
- 处置级:剩余后备时间低于5分钟。触发服务器群安全关机脚本,医疗设备端由人工决策是否启动应急转移。
推送通道用的是企业微信机器人,省去自建短信网关的麻烦。NUT本身支持NOTIFYCMD回调,可以在事件发生的瞬间执行自定义脚本。一个实用技巧是:事件脚本里只做“转发”,把原始告警文本塞进MQTT,再由单独的消息中台决定走企业微信、邮件还是短信。这样告警通道可以灵活调整,不会因为某个第三方接口挂了就丢掉整条告警链路。
4. 从零开始部署:影像科三台设备的完整实录
4.1 摸底与布线阶段
我先整理了影像科三台UPS的通信接口情况:CT室一台20kVA,带SNMP管理卡;DR室两台3kVA,都是USB口。驱动方案很明确,20kVA那台用snmp-ups驱动,3kVA的两台用usbhid-ups驱动。
探针盒选了两台树莓派4B,一台放CT设备间,一台放DR设备间。布线阶段有个大坑要提醒:UPS随箱的USB线通常很短,但设备间往往和值班室隔着几十米。我的做法是每台UPS附近放一台探针盒,探针盒通过网线回传数据,而不是试图把USB线延长十几米。USB延长线超过5米就会出现通信不稳定,这是物理层的硬限制,别指望靠驱动能救回来。
4.2 驱动配置与首轮验证
SNMP型号在ups.conf里这样写:
[ct-ups] driver = snmp-ups port = 192.168.10.50 community = public snmp_version = v2cUSB型号就写usbhid-ups,port设成auto。配置完后启动服务:
sudo systemctl restart nut-server sudo systemctl restart nut-client upsc ct-ups@localhost我特别建议:一定要先跑upsc手动验证,逐项检查battery.charge、battery.runtime、ups.status这几个关键字段是否正常,再进入上层应用的联调。很多问题其实在驱动阶段就能暴露,不要急着写监控页面。我们当时就遇到一台老型号UPS,usbhid-ups驱动读不到battery.runtime,换了mge-usb驱动才正常。这种事情越早发现越省事。
4.3 应用层采集服务与可视化看板
Python采集服务用的是最简单的轮询模型:
import time import subprocess import pymysql def ups_status(name): out = subprocess.check_output( ["upsc", f"{name}@localhost"] ).decode() d = {} for line in out.splitlines(): k, v = line.split(": ", 1) d[k] = v return d def save_to_db(u, data): # 写入MySQL,按时间戳落库 pass while True: for u in ["ct-ups", "dr-ups1", "dr-ups2"]: data = ups_status(u) save_to_db(u, data) time.sleep(5)这里用不着上什么高级框架,subprocess调upsc拿到标准字段,就是最稳定的接口。存库选了MySQL,数据量完全没压力,5秒一轮三台设备一天也就五万条记录。
可视化用的是Grafana,数据源直连MySQL。看板主要放五个面板:电池剩余百分比、后备剩余时间、负载率、输入输出电压、最近断电事件列表。另外在临床科室护士站的大屏放一个简化版,只显示“当前正常 / 电池供电中 / 剩余xx分钟”。信息越少越容易被值班人员消化,这个设计原则我至今觉得是对的。
4.4 联动脚本与应急演练
做联动脚本前先定了一个紧急预案表:
| 层级 | 触发条件 | 动作 |
|---|---|---|
| 三级 | 市电中断,电池供电 | 推送告警,声光报警器亮起 |
| 二级 | 剩余时间低于10分钟 | 再次推送,电话通知设备科值班 |
| 一级 | 剩余时间低于5分钟 | 服务器群安全关机,医疗设备由人工处置 |
第一次演练我直接断开了市电输入,观察系统表现:5秒内弹出告警,10秒内写入数据库,30秒内值班手机收到推送。电池放电到50%时手动恢复市电,整个流程算跑通。
这种演练强烈建议每季度做一次,而且要换人操作。真实断电发生时,在现场的往往不是搭这套系统的人,操作人越熟悉应急流程,系统价值才越大。
5. 常见问题与排查技巧实录
5.1 USB通信频繁掉线
这是出现频率最高的问题。现象是NUT日志里出现Device disconnected,upsc偶尔取不到数据。原因排序:USB线质量差、驱动误判、树莓派供电不足。
排查步骤:
- 换一根带磁环的粗USB线,不要用随机短线的劣质替换品。
- 检查树莓派电源,5V/3A的适配器必须到位,供电不足会导致外设反复枚举。
- 用dmesg查看设备是否反复断开重连。
我还用过一个小技巧:把USB接口的自动挂起功能关掉,在/etc/udev/rules.d/下新建规则:
ACTION=="add", SUBSYSTEM=="usb", TEST=="power/control", ATTR{power/control}="on"这个规则对不少USB掉线问题立竿见影,尤其是树莓派这类单板机上。
5.2 电量百分比跳变
现象是battery.charge从70%突然跳到99%,或者反过来。原因是UPS固件采样算法的平滑度不够,加上电池劣化后电压曲线非线性,导致百分比出现台阶式跳变。
处理办法是加“去抖”逻辑。我在采集服务里加了几行判断:连续三次采样都在同一个区间才更新状态;剩余时间低于10分钟的告警判断改为连续两轮均低于10分钟才触发。这样能有效避免单次数据异常导致的误告警。有一次凌晨三点告警把我叫醒,起来一看UPS状态正常,就是跳变触发的问题,加了去抖之后再没发生过。
5.3 battery.runtime虚高
UPS显示剩余时间40分钟,实际放电15分钟就关机了,这是电池老化后最常见的坑。厂家估算基于新电池容量,电池用了三年后实际容量可能只剩60%,固件却还按新电池算。
我的解决方案是在应用层维护一张电池健康度表,每次做完整放电测试后回写一次实测后备时间。监控页面上优先展示实测校准值,而不是固件原始值。校准值的获取方式:做一次完整的放电测试,从电池供电开始计时,到低压报警或接近设定的放电下限为止,记录实际分钟数和当时的平均负载。后续每次真实断电,系统也会自动更新这张表,展示的剩余时间会越来越准。
5.4 常见问题速查表
| 症状 | 可能原因 | 处理方式 |
|---|---|---|
| upsc能通但没有battery字段 | 驱动型号不匹配 | 换usbhid-ups或mge-usb驱动,重新探测 |
| SNMP读不到数据 | community不匹配、端口被防火墙拦截 | 检查UDP 161端口与public字符串 |
| upsmon频繁上报电池模式 | 输入电压波动导致误触发 | 调高输入电压预警阈值 |
| 树莓派重启后NUT不自动启动 | 服务未启用或依赖顺序错误 | systemctl enable nut-server,确认网络服务就绪 |
| 告警只发了一次后续没再发 | 缺少去重和状态变化逻辑 | 事件状态变化时才触发推送,不要周期重发 |
6. 开源改造与后续扩展
6.1 源码结构说明
我把整套系统的源码、配置文件、Grafana看板JSON都整理成了开源项目,目录结构大致是:
medical-ups-monitor/ ├── nut/ # NUT配置示例 ├── collector/ # Python采集服务 ├── scripts/ # 联动与关机脚本 ├── dashboard/ # Grafana看板JSON └── docs/ # 部署文档与拓扑图开源的价值在于,医院设备科普遍人手紧张,没人有精力从零写一套协议解析和采集框架。基于NUT这个成熟底座,我实际做的事只有三件:写采集服务、配告警规则、做展示看板。这才是医院场景真正需要定制的地方,其他部分直接用社区项目就好,不要重复造轮子。
6.2 扩展方向与开源边界
后续扩展我考虑了三个方向:
- 接入更多设备类型:把精密空调、漏水检测、配电柜状态一起纳入,构成完整的动力环境监控平台。
- 引入电池寿命预测:用历史放电数据做简单的容量衰减曲线拟合,这个需要积累几个月的数据才有实际意义。
- 与医院信息系统打通:断电事件自动生成工单,和资产管理系统联动,让换电池、巡检维修的流程自动化。
开源许可证我选的是MIT,医院场景不存在版权顾虑,系统集成商拿去做商业交付也没有障碍。这里我要多说一句边界问题:这套系统在医疗机构里的定位是“运维工具”,不是“医疗设备本身”。它做的是监测、预警、辅助决策,绝不要试图让它去替代任何医疗设备的安全机制。把这个边界想清楚,很多设计上的纠结就自然解开了。
这套系统上线大半年,我的体会是:比UPS本身更重要的,是让团队随时知道UPS的真实状态。它不会让你避免停电,但能让你在停电的每一个阶段都掌握主动权。以后你值班时手机里收到的,不该是黑暗中打来的求助电话,而应该是一条提前到达的预警信息。希望这套开源方案,也能帮你做到这件事。