公司动态
群晖NAS与UPS智能联动:基于NUT协议实现深度监控与自动化
1. 从一次意外断电说起为什么需要关注UPS与NAS的联动去年夏天我经历了一次至今想起来都心有余悸的停电。当时我正在外地出差家里的主力NAS网络附加存储——一台运行着大量Docker容器、虚拟机并且存储了多年工作资料和家庭照片的群晖设备毫无征兆地因为市电故障而直接关机。等我远程发现并联系家人处理时已经过去了好几个小时。虽然数据盘本身有RAID保护但非正常关机带来的文件系统检查fsck耗时漫长更让我担心的是那些正在运行的数据库和虚拟机状态是否一致。这次事件让我深刻意识到对于家庭或小型办公室的数据中心而言一个可靠的UPS不间断电源以及一套完善的自动关机机制其重要性不亚于硬盘本身的质量。大多数朋友给NAS配UPS可能只做到了第一步插上电源线让NAS在停电时能继续运行一段时间。但这远远不够。真正的价值在于“智能联动”——让UPS能够主动通知NAS“市电断了电池还能撑X分钟请开始你的安全关机流程。” 而实现这一通信的桥梁就是NUTNetwork UPS Tools。群晖DSM系统内置了对NUT客户端的良好支持可以方便地连接支持USB通信的UPS实现自动关机。然而这个“方便”也带来了限制你只能使用群晖提供的那个简单的图形界面被动地接收UPS的状态却无法主动、灵活地获取更详细的数据或者将这些数据集成到你自己的智能家居、监控系统中。这就是我进行“群晖UPS NUT接口开发探索”的初衷。我不满足于仅仅让NAS安全关机我还想知道当前电池电量多少负载功率多大输入电压是否正常停电事件发生的具体时间我想把这些数据记录下来做成图表甚至设置更精细的告警规则比如电池健康度下降时提前预警。而这一切都需要绕过DSM的图形界面直接与底层运行的NUT服务对话。这个过程就像给你的NAS装上了一双“眼睛”让它不仅能“听”UPS的关机指令还能“看”清UPS的每一个运行细节。2. 理解NUT不只是群晖的专属管家在深入操作之前我们必须先搞清楚NUT到底是什么以及它在群晖系统中是如何工作的。这能帮助我们明白我们即将探索的接口位于整个技术栈的哪一层。NUT是一个开源、跨平台的套件专门用于管理不同品牌、型号的UPS。它的设计采用了经典的C/S客户端/服务器架构非常灵活UPS驱动层负责与具体的UPS硬件通过USB、串口等方式通信读取状态信息电压、负载、电量等并执行指令如关机、自检。这部分在群晖上通常由usbhid-ups这类驱动模块完成。NUT服务端在群晖上这个角色通常由upsd守护进程担任。它监听网络或本地套接字对外提供UPS的状态数据。你可以把它理解为一个“数据代理”。NUT客户端任何能够连接upsd并理解其协议的程序都可以是客户端。群晖DSM的管理界面就是一个图形化的客户端。而我们想要开发的是另一个自定义的客户端。在默认的群晖UPS设置中DSM启用的是NUT的“主模式”。简单来说你的群晖NAS既是NUT服务端upsd也是连接本地UPS的客户端。所有配置通过DSM的图形界面完成并写入/usr/syno/etc/ups/ups.conf等配置文件。之后upsd进程启动并通过USB读取UPS数据同时提供一个本地服务。我们的目标就是找到这个upsd服务提供的访问接口然后用自己的程序比如一个Python脚本去连接它、查询数据而完全不需要触动DSM本身的配置。这样做有几个巨大优势无侵入性不修改任何群晖系统文件不影响DSM自带UPS功能的正常工作安全系数高。数据自由度可以获得比DSM界面显示更丰富、更原始的数据字段。集成灵活可以将数据轻松推送到Home Assistant、Prometheus、Grafana或自建的数据看板实现深度监控。注意不同DSM版本和机型NUT的配置路径和细节可能略有差异。本文基于DSM 7.x版本进行探索核心思路在6.x及后续版本中基本通用。3. 探明路径定位群晖上的NUT服务接口知道了原理下一步就是实战侦察。我们需要登录到群晖的SSH后台需在DSM控制面板中启用SSH服务像侦探一样找出NUT服务的蛛丝马迹。3.1 确认NUT服务状态与配置首先检查NUT相关的进程是否在运行ps aux | grep -E (upsd|upsmon)你应该能看到upsd进程在运行其参数中通常会包含配置文件路径例如-u root -c /usr/syno/etc/ups/ups.conf。这个路径就是关键。接着查看核心配置文件了解UPS是如何被定义的cat /usr/syno/etc/ups/ups.conf这个文件的内容通常很简单DSM为你的UPS自动创建了一个配置段。例如如果你的UPS是APC品牌可能会看到类似下面的内容[ups] driver usbhid-ups port auto desc My UPS这里的[ups]就是你的UPS在NUT系统中的名称也叫作“UPS标识名”。记下这个名字后续连接时会用到。在我的环境中它就是简单的ups。然后查看upsd的访问控制配置这决定了谁可以连接以及如何连接cat /usr/syno/etc/ups/upsd.conf重点关注LISTEN指令和ACL规则。在群晖的默认配置中你很可能看到LISTEN 127.0.0.1 3493 LISTEN ::1 3493这表示upsd服务只监听在本机localhost的3493端口上。3493是NUT服务的默认端口。ACL规则部分DSM通常会配置允许本地用户连接。3.2 使用NUT原生工具进行首次连接测试在动手写代码前先用NUT自带的命令行客户端验证一下接口是否通畅。这能让我们快速确认基础环境。首先需要安装NUT的客户端工具。群晖的ipkg包管理器可能没有或者版本旧。更可靠的方式是使用entware一个为嵌入式设备提供的软件仓库。如果你还没安装entware可以搜索相关教程进行安装这里假设你已经安装好。通过entware安装NUT客户端opkg update opkg install nut-client安装完成后使用upsc命令查询UPS状态。这是最直接的方法upsc upslocalhost命令解释upsc是查询工具ups是我们在ups.conf里看到的UPS标识名localhost表示连接本机的NUT服务。如果一切正常你会看到瀑布般刷出几十行数据例如battery.charge: 100 battery.charge.low: 10 battery.runtime: 2800 input.voltage: 223.0 input.voltage.nominal: 230 output.voltage: 230.0 ups.load: 15 ups.status: OL ...每一行都是一个“变量VAR”及其“值VALUE”。OL代表在线On LineOB代表电池供电On Battery。这个列表就是NUT服务提供的全部数据宝藏。成功执行这条命令标志着我们已经找到了“接口大门”并且拿到了“钥匙”。4. 从命令行到代码构建自定义的NUT客户端手动敲命令不是长久之计。我们的目标是自动化、周期性地获取这些数据。这里我选择用Python来实现因为它语法简洁库丰富非常适合做系统集成和数据处理。4.1 环境准备与基础连接在群晖上你可以选择在DSM的“任务计划”中创建运行Python脚本的任务或者更优雅一点在Docker容器中运行。为了不影响系统环境我推荐使用Docker。这里假设你在群晖上已经安装了Docker套件。首先创建一个项目目录比如/docker/nut-monitor然后创建我们的Python脚本nut_client.py。脚本的第一步是建立与NUT服务的连接并获取数据。NUT协议本身是基于文本的我们可以直接用socket连接3493端口发送命令解析返回的文本。但幸运的是有现成的Python库可以简化这个过程比如nut2或pynut。不过为了更深入地理解协议并避免依赖外部库在群晖精简环境里更可靠我选择使用Python标准库的socket来实现一个简易版本。#!/usr/bin/env python3 简易NUT客户端用于从群晖本机upsd服务获取UPS状态。 import socket import sys def get_ups_vars(ups_nameups, host127.0.0.1, port3493, timeout5): 连接NUT服务端获取指定UPS的所有变量。 参数: ups_name: UPS在ups.conf中的标识名默认为ups host: upsd服务地址默认为本机 port: upsd服务端口默认为3493 timeout: 连接和读取超时时间秒 返回: 一个字典键为变量名值为变量值。如果失败返回None。 data_dict {} try: # 创建TCP socket连接 sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) sock.connect((host, port)) # NUT协议首先发送一个包含用户名和密码的LIST命令。 # 在群晖默认配置下通常允许本地无密码或使用默认密码连接。 # 我们可以先尝试无密码查询。如果被拒绝再尝试其他方式。 command fLIST VAR {ups_name}\n sock.sendall(command.encode(utf-8)) # 接收响应数据 response b while True: chunk sock.recv(1024) if not chunk: break response chunk # NUT协议响应以END LIST VAR {ups_name}结束 if fEND LIST VAR {ups_name}.encode(utf-8) in response: break sock.close() # 解析响应 response_text response.decode(utf-8, errorsignore) lines response_text.split(\n) for line in lines: line line.strip() # 响应行格式: VAR ups_name var_name var_value if line.startswith(fVAR {ups_name}): parts line.split( , 3) # 最多分割成4部分 if len(parts) 4: var_name parts[2] # 去掉值两端的引号 var_value parts[3].strip() data_dict[var_name] var_value return data_dict except socket.timeout: print(f错误连接或读取超时{host}:{port}) return None except ConnectionRefusedError: print(f错误连接被拒绝。请确认upsd服务正在运行并监听在{host}:{port}) return None except Exception as e: print(f未知错误{e}) return None if __name__ __main__: # 使用默认参数获取数据 ups_data get_ups_vars() if ups_data: print(成功获取UPS状态) # 打印一些关键信息 for key in [battery.charge, battery.runtime, input.voltage, ups.load, ups.status]: if key in ups_data: print(f{key}: {ups_data[key]}) # 你也可以打印全部数据 # for key, value in ups_data.items(): # print(f{key}: {value}) else: print(获取UPS状态失败。) sys.exit(1)将这段代码保存到nut_client.py。在群晖SSH中进入脚本目录尝试运行python3 nut_client.py你应该能看到类似之前upsc命令输出的关键信息。至此最核心的数据获取功能就实现了。4.2 处理认证与错误增强上面的简易版本假设了无密码访问。但有些UPS驱动或配置可能需要密码。NUT的认证信息通常存储在/usr/syno/etc/ups/upsd.users中。我们可以检查一下cat /usr/syno/etc/ups/upsd.users如果这个文件存在且有内容你可能需要修改连接命令。在socket连接后需要先发送USERNAME user和PASSWORD pass命令进行登录然后再发送LIST VAR命令。为了脚本的健壮性我们可以增加一个带认证的版本并添加更完善的错误处理和日志功能。这里提供一个增强版的连接函数思路def get_ups_vars_auth(ups_nameups, host127.0.0.1, port3493, usernameNone, passwordNone, timeout5): sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) sock.connect((host, port)) # 读取欢迎信息可选 # welcome sock.recv(1024) # 如果有用户名密码则进行认证 if username: sock.sendall(fUSERNAME {username}\n.encode()) resp sock.recv(1024) # 期待返回 OK if password: sock.sendall(fPASSWORD {password}\n.encode()) resp sock.recv(1024) # 期待返回 OK # 后续的LIST VAR命令与之前相同 # ...在实际的群晖默认配置中upsd.users文件常常不存在或为空这意味着允许本地无密码访问。所以我们的简易版本在大多数情况下是可行的。5. 数据落地与应用从监控到告警获取到数据只是第一步让数据产生价值才是目的。我们可以从以下几个方向进行拓展5.1 定时任务与数据记录最简单的应用是将关键数据定期记录到日志文件或数据库中。我们可以利用群晖自带的“任务计划”功能。创建脚本目录在群晖的某个共享文件夹下如docker或scripts存放最终的Python脚本例如/volume1/docker/scripts/ups_monitor.py。安装Python环境确保任务计划能调用Python。最稳妥的方法是使用群晖DSM自带的Python3通常位于/usr/bin/python3或/usr/local/bin/python3。可以通过SSH运行which python3确认路径。创建计划任务打开DSM的“控制面板” - “任务计划”。点击“新增” - “计划的任务” - “用户定义的脚本”。在“常规”选项卡给任务起个名字如“UPS状态监控”。在“计划”选项卡设置运行频率例如“每5分钟”。在“任务设置”选项卡用户账户选择“root”因为需要访问本地网络端口在“运行命令”区域输入/usr/bin/python3 /volume1/docker/scripts/ups_monitor.py /volume1/docker/scripts/ups_monitor.log 21这个命令会执行脚本并将所有输出包括错误追加到日志文件中。完善监控脚本修改之前的nut_client.py增加数据解析和日志记录功能。例如将数据以JSON格式和当前时间戳一起写入文件或者直接写入SQLite数据库。# ups_monitor.py 部分增强代码示例 import json import time from datetime import datetime def main(): data get_ups_vars() if not data: return # 提取关键指标 snapshot { timestamp: datetime.now().isoformat(), battery_charge: int(data.get(battery.charge, 0)), battery_runtime: int(data.get(battery.runtime, 0)), input_voltage: float(data.get(input.voltage, 0)), ups_load: int(data.get(ups.load, 0)), ups_status: data.get(ups.status, UNKNOWN), } # 1. 写入JSON行文件便于后续处理 log_file /volume1/docker/scripts/ups_history.jsonl with open(log_file, a) as f: f.write(json.dumps(snapshot) \n) # 2. 简单控制台输出会被任务计划重定向到日志 print(f[{snapshot[timestamp]}] 状态:{snapshot[ups_status]} 电量:{snapshot[battery_charge]}% 负载:{snapshot[ups_load]}%) # 3. 简单的阈值告警例如电量低于30%时记录警告 if snapshot[ups_status] OB and snapshot[battery_charge] 30: warning_msg f警告UPS正在使用电池供电电量仅剩{snapshot[battery_charge]}% print(warning_msg) # 这里可以集成发送邮件或通知的功能见下一节 if __name__ __main__: main()5.2 集成到智能家居与可视化平台记录下来的数据可以非常方便地集成到更强大的系统中。推送到Home Assistant如果你使用Home Assistant作为智能家居中枢可以通过其RESTful Sensor或Command Line Sensor来获取UPS数据。我们的Python脚本可以作为一个简单的HTTP服务运行或者直接让HA定期执行脚本并解析输出。方法A推荐在Python脚本中启动一个简单的HTTP服务器如使用Flask提供一个返回JSON数据的API端点如/api/ups。然后在HA配置中添加一个rest传感器指向这个本地API。方法B在HA的configuration.yaml中定义一个command_line传感器直接调用我们写好的Python脚本或一个封装了脚本输出的Shell脚本并解析其JSON输出。使用InfluxDB Grafana这是打造专业级监控看板的黄金组合。在群晖Docker中部署InfluxDB和Grafana容器。修改我们的ups_monitor.py脚本在获取数据后使用influxdb客户端库将数据点写入InfluxDB。在Grafana中配置数据源为InfluxDB然后就可以自由地创建仪表盘了。你可以绘制电池电量曲线、负载功率曲线、输入电压波动图并设置丰富的告警规则如电池运行时间低于10分钟时触发告警。5.3 实现高级告警超越DSM的简单通知DSM自带的UPS通知只能在“进入电池模式”和“电池电量低”时触发且渠道有限。通过自定义脚本我们可以实现更精细的告警电池健康度预警监控battery.charge.low低电量阈值或battery.voltage等指标结合使用时长预测电池是否需要更换。异常电压告警持续监控input.voltage如果电压持续过高或过低即使未停电可能意味着市电质量有问题可以提前预警。负载过重告警监控ups.load如果负载长期接近UPS额定功率的80%以上提示风险。多通道通知除了DSM的邮件和短信我们可以集成微信通过ServerChan、企业微信应用、钉钉、Telegram Bot等更即时、更常用的通知方式。实现告警的逻辑可以放在ups_monitor.py中在检测到异常状态时调用另一个发送通知的脚本或函数。6. 避坑指南与实战心得在探索和部署这套自定义监控系统的过程中我踩过不少坑也总结了一些经验。6.1 权限与路径问题脚本执行权限确保你的Python脚本有可执行权限chmod x ups_monitor.py并且在任务计划中指定的路径完全正确。在DSM的Web界面创建任务时路径是相对于NAS根文件系统的而不是你登录SSH后看到的家目录。Python环境在任务计划中务必使用绝对路径指定Python解释器如/usr/bin/python3。不要依赖python3这个命令因为任务计划运行时的环境变量可能与SSH会话不同。文件写入权限如果你的脚本需要写入日志或数据文件请确保运行任务的用户通常是root对该文件所在目录有写权限。最好将输出文件放在共享文件夹如/volume1/...下而不是系统目录。6.2 NUT服务连接失败排查如果upsc或自定义脚本无法连接按以下步骤排查确认服务运行ps aux | grep upsd。如果没运行可能是DSM的UPS服务未启用先去DSM控制面板里检查并启用。确认监听端口netstat -tlnp | grep 3493。查看upsd是否在127.0.0.1:3493上监听。如果没有检查upsd.conf配置文件。检查防火墙虽然连接的是本机但极端情况下也要确认DSM或系统防火墙没有阻止本地回环地址的连接。尝试使用localhost或127.0.0.1在脚本中连接地址优先使用127.0.0.1比localhost更可靠。6.3 数据字段解读与兼容性不同品牌、型号的UPS通过NUT驱动上报的变量名和数量可能有差异。APC、山特、CyberPower等常见品牌支持较好但一些字段可能缺失。务必在测试阶段用upsc命令完整查看你的UPS支持哪些变量。重点关注以下几个核心变量ups.status: 这是最重要的状态指标。OL在线OB电池供电OL CHRG在线且充电中OB DISCHRG电池供电且放电中OFF关机等。battery.charge: 电池剩余容量百分比。battery.runtime: 预估的剩余运行时间秒。这个值在某些UPS上可能不准仅供参考。input.voltage: 当前输入电压。可用于判断市电质量。ups.load: 当前负载占UPS额定容量的百分比。battery.voltage/battery.current: 电池电压/电流用于评估电池健康状态。编写脚本时对关键变量的获取要使用data.get(key, default_value)的方式并提供合理的默认值避免因为某个字段缺失导致脚本崩溃。6.4 性能与稳定性考量查询频率不要设置过高的查询频率如每秒一次。对于UPS状态监控每30秒到5分钟查询一次完全足够。过于频繁的查询可能对老旧的UPS或USB控制器造成不必要的负担。错误处理与重试网络连接或服务暂时不可用是可能的。脚本中必须有完善的try...except块并且对于偶发的连接失败可以考虑加入简单的重试机制例如重试2次间隔2秒而不是一次失败就彻底停止。日志轮转如果直接将日志写入文件要定期清理或设置日志轮转避免磁盘空间被占满。可以使用logrotate工具或者简单地在脚本中判断文件大小超过一定阈值后归档旧日志。通过这一系列的探索我成功地将那台“沉默”的UPS变成了家庭数据中心里一个活跃的、可观测的智能节点。现在我不仅能在Grafana仪表盘上实时看到它的每一个心跳还能在电池健康度下降、市电电压异常时第一时间收到更灵活的通知。更重要的是整个过程完全独立于DSM系统做到了无侵入、可定制、高可用。如果你也对自己的数据安全有一份执着不妨也动手试试给你的NAS和UPS之间搭建一座更智能的数据桥梁。