适用于:飞牛OS (fnOS) 系统 + NUT (Network UPS Tools)
测试系统版本:0.9.12
本功能使用AI生成,lz只测试过ONBATT和ONLINE功能正常,其他需要自行测试,如有问题可发帖询问。能力有限,尽量解决问题
一、功能目标
- 实时检测 UPS 事件(如断 电、恢复、低电量等)
- 提取事件发生时间并发送带时间戳的通知邮件
- 使用 HTML 格式美化邮件内容
- 定期自动清理日志和状态文件
- 避免重复通知旧事件(通过记录清理时间戳)
二、涉及组件
组件 |
路径 |
说明 |
监控脚本 |
/usr/local/bin/ups_event_monitor.sh |
检测 syslog 中的 UPS 事件 |
通知脚本 |
/etc/nut/notifycmd |
发送 HTML 邮件通知 |
日志文件 |
/var/log/ups_event_monitor.log |
主要监控日志 |
状态文件 |
/tmp/ups_event_state/ |
存储事件时间戳,防止重复通知 |
清理脚本 |
/usr/local/bin/cleanup_ups_logs.sh |
清空日志并记录清理时间 |
定时任务 |
crontab -l |
自动执行监控和清理 |
三、安装所需依赖
所需依赖一览
组件 |
用途 |
安装包名(OpenWrt) |
安装包名(Debian/Ubuntu) |
jq |
JSON 解析工具 |
jq |
jq |
msmtp |
邮件发送客户端 |
msmtp |
msmtp |
使用以下命令检查是否安装所需依赖
# 1. 检查 jq 是否可用
jq --version
# 示例输出:jq-1.6
2. 检查 msmtp 是否可用
msmtp --version
# 示例输出:msmtp version 1.4.32
使用以下命令安装所需依赖
sudo apt update
# 安装 jq
sudo apt install jq -y
# 安装 msmtp
sudo apt install msmtp -y
设置 msmtp 配置文件
# 设置msmtp全局配置
sudo vim /etc/msmtprc
msmtp 配置文件 /etc/msmtprc
:
defaults
tls on
tls_starttls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
account default
# 填写 SMTP 服务器地址,比如使用qq邮箱发送邮件,这里就填:host smtp.qq.com
host smtp.host.com
port 587
auth on
# 收件人邮件地址
user your_email@example.com
# 密码/授权码,163、qq邮箱等使用授权码,可以上网搜索对应邮箱授权码如何获取
password your_password
# 发件人邮件地址,可与收件人邮件地址相同
from your_email@example.com
三、关键脚本说明
1. 通知脚本:/etc/nut/notifycmd
#!/bin/bash
# ===== 配置区 =====
SYSLOG_FILE="/var/log/syslog"
NOTIFY_SCRIPT="/etc/nut/notifycmd"
LOGFILE="/var/log/ups_event_monitor.log"
STATE_DIR="/tmp/ups_event_state"
mkdir -p "$STATE_DIR"
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') [$1] $2" >> "$LOGFILE"
}
# 定义事件匹配规则(事件类型 => eventId => 消息)
declare -A EVENT_CONFIG=(
["ONBATT"]="UPS_ONBATT_LOWBATT 市电中断,已切换至电池供电"
["LOWBATT"]="UPS_BATTERY_LOW 电池电量低,请检查电源状态"
["FSD"]="UPS_FINAL_SHUTDOWN 即将关机,设备将在短时间内断 电"
["ONLINE"]="UPS_ONLINE 市电已恢复,UPS 正常运行"
["COMMBAD"]="UPS_COMM_LOST 与 UPS 的通信已中断,请检查连接"
["COMMOK"]="UPS_COMM_RESTORED 与 UPS 的通信已恢复"
)
# 遍历所有事件类型
for EVENT_TYPE in "${!EVENT_CONFIG[@]}"; do
IFS=' ' read -r -a config <<< "${EVENT_CONFIG[$EVENT_TYPE]}"
EVENT_ID="${config[0]}"
DEFAULT_MSG="${config[1]}"
# 提取最新一行含 EVENT_ID 的日志,并只保留 JSON 部分
LOG_LINE=$(grep -a "$EVENT_ID" "$SYSLOG_FILE" | tail -n1)
if [ -z "$LOG_LINE" ]; then
log "$EVENT_TYPE" "❌ 未发现事件关键词"
continue
fi
# 提取完整的 JSON 部分(从第一个 '{' 开始到最后)
JSON_PART=$(echo "$LOG_LINE" | grep -o '{.*}')
if [ -z "$JSON_PART" ] || ! echo "$JSON_PART" | jq . >/dev/null 2>&1; then
log "$EVENT_TYPE" "⚠️ JSON 提取失败或无效"
log "$EVENT_TYPE" "🔍 原始日志行: $LOG_LINE"
continue
fi
NEW_EVENT_ID=$(echo "$JSON_PART" | jq -r '.eventId')
NEW_EVENT_TIME=$(echo "$JSON_PART" | jq -r '.datetime')
if [ "$NEW_EVENT_ID" != "$EVENT_ID" ]; then
log "$EVENT_TYPE" "⚠️ eventId 不匹配: $NEW_EVENT_ID vs $EVENT_ID"
continue
fi
if [ -z "$NEW_EVENT_TIME" ] || [ "$NEW_EVENT_TIME" = "null" ]; then
log "$EVENT_TYPE" "⚠️ 无法提取时间戳"
continue
fi
STATE_FILE="$STATE_DIR/last_event_$EVENT_TYPE.timestamp"
if [ -f "$STATE_FILE" ]; then
LAST_EVENT_TIME=$(cat "$STATE_FILE")
else
LAST_EVENT_TIME=0
fi
# 加载最后清理时间戳(如果存在)
CLEANUP_TIME=0
TIMESTAMP_FILE="$STATE_DIR/last_cleanup.timestamp"
if [ -f "$TIMESTAMP_FILE" ]; then
CLEANUP_TIME=$(cat "$TIMESTAMP_FILE")
fi
# 如果事件时间早于最后一次清理时间,则跳过
if (( NEW_EVENT_TIME < CLEANUP_TIME )); then
log "$EVENT_TYPE" "⏰ 事件发生在清理时间之前,跳过重复通知"
continue
fi
if (( NEW_EVENT_TIME > LAST_EVENT_TIME )); then
EVENT_TIME=$(date -d "@$NEW_EVENT_TIME" "+%Y-%m-%d %H:%M:%S")
log "$EVENT_TYPE" "🔔 检测到新事件,时间戳: $NEW_EVENT_TIME ($EVENT_TIME)"
NOTIFYTYPE="$EVENT_TYPE" \
NOTIFYMSG="$DEFAULT_MSG" \
NOTIFYDATETIME="$NEW_EVENT_TIME" \
"$NOTIFY_SCRIPT"
if [ $? -eq 0 ]; then
log "$EVENT_TYPE" "✅ 成功发送通知"
echo "$NEW_EVENT_TIME" > "$STATE_FILE"
else
log "$EVENT_TYPE" "❌ 通知执行失败"
fi
else
log "$EVENT_TYPE" "⏳ 已处理过该事件,跳过重复通知"
fi
done
admin@fnos:/vol3/1000/appdata/nginx-proxy-manager$ ^C
admin@fnos:/vol3/1000/appdata/nginx-proxy-manager$ cat /etc/nut/notifycmd
#!/bin/bash
# ===== 配置区 =====
EMAIL_RECIPIENT="17667192839@163.com"
LOGFILE="/var/log/nut/notifycmd.log"
# 创建日志目录和文件
mkdir -p /var/log/nut
touch "$LOGFILE"
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') [$NOTIFYTYPE] $1" >> "$LOGFILE"
}
# 将 Unix 时间戳转换为可读格式
if [ -n "$NOTIFYDATETIME" ]; then
EVENT_TIME=$(date -d @"$NOTIFYDATETIME" "+%Y-%m-%d %H:%M:%S")
else
EVENT_TIME=$(date '+%Y-%m-%d %H:%M:%S')
fi
TITLE="🚨 UPS 状态更新:$NOTIFYTYPE"
log "🔔 收到通知类型: $NOTIFYTYPE"
log "🕘 事件时间: $EVENT_TIME"
log "📝 消息内容: $NOTIFYMSG"
function send_email() {
log "📧 正在尝试发送邮件到 $EMAIL_RECIPIENT"
# 构建 HTML 内容
HTML_CONTENT="
<html>
<head>
<meta charset='UTF-8'>
<title>UPS 状态通知</title>
</head>
<body style='font-family: Arial, sans-serif; background-color: #fafafa; padding: 20px;'>
<div style='max-width: 600px; margin: auto; background-color: #fff; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); padding: 20px;'>
<h2 style='color:#e67e22;'>🚨 UPS 状态更新:$NOTIFYTYPE</h2>
<table border='0' cellpadding='8' cellspacing='0' width='100%' style='border-collapse: collapse;'>
<tr><td style='background-color:#f2f2f2; width:30%;'><strong>事件类型:</strong></td><td>$NOTIFYTYPE</td></tr>
<tr><td style='background-color:#f2f2f2;'><strong>事件时间:</strong></td><td>$EVENT_TIME</td></tr>
<tr><td style='background-color:#f2f2f2;'><strong>事件描述:</strong></td><td>$NOTIFYMSG</td></tr>
<tr><td style='background-color:#f2f2f2;'><strong>UPS 名称:</strong></td><td>$UPSNAME</td></tr>
<tr><td style='background-color:#f2f2f2;'><strong>系统时间:</strong></td><td>$(date '+%Y-%m-%d %H:%M:%S')</td></tr>
</table>
<p style='margin-top:20px; color:#555;'>请及时处理。</p>
</div>
</body>
</html>
"
echo "$HTML_CONTENT" | msmtp "$EMAIL_RECIPIENT" <<EOF
To: $EMAIL_RECIPIENT
Subject: $TITLE
Content-Type: text/html; charset=UTF-8
$(cat)
EOF
if [ $? -eq 0 ]; then
log "✅ 邮件发送成功"
else
log "❌ 邮件发送失败 (msmtp error)"
fi
}
case $NOTIFYTYPE in
ONBATT|LOWBATT|FSD|ONLINE|COMMOK|COMMBAD)
send_email
;;
*)
log "❓ 未知通知类型,未发送邮件"
;;
esac
功能:
- 接收来自 NUT 的通知参数;
- 构建 HTML 格式的邮件内容;
- **使用 **
msmtp
发送邮件;
- **记录通知日志到 **
/var/log/nut/notifycmd.log
;
示例邮件内容(HTML):
<h2 style="color:#e67e22;">🚨 UPS 状态更新:ONBATT</h2>
<table border="0" cellpadding="8" cellspacing="0" width="100%">
<tr><td style="background-color:#f2f2f2;"><strong>事件类型:</strong></td><td>ONBATT</td></tr>
<tr><td style="background-color:#f2f2f2;"><strong>事件时间:</strong></td><td>2025-06-30 00:05:51</td></tr>
<tr><td style="background-color:#f2f2f2;"><strong>事件描述:</strong></td><td>市电中断,已切换至电池供电</td></tr>
<tr><td style="background-color:#f2f2f2;"><strong>UPS 名称:</strong></td><td>ups1</td></tr>
</table>
2. 监控脚本:/usr/local/bin/ups_event_monitor.sh
#!/bin/bash
# ===== 配置区 =====
SYSLOG_FILE="/var/log/syslog"
NOTIFY_SCRIPT="/etc/nut/notifycmd"
LOGFILE="/var/log/ups_event_monitor.log"
STATE_DIR="/tmp/ups_event_state"
mkdir -p "$STATE_DIR"
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') [$1] $2" >> "$LOGFILE"
}
# 定义事件匹配规则(事件类型 => eventId => 消息)
declare -A EVENT_CONFIG=(
["ONBATT"]="UPS_ONBATT_LOWBATT 市电中断,已切换至电池供电"
["LOWBATT"]="UPS_BATTERY_LOW 电池电量低,请检查电源状态"
["FSD"]="UPS_FINAL_SHUTDOWN 即将关机,设备将在短时间内断 电"
["ONLINE"]="UPS_ONLINE 市电已恢复,UPS 正常运行"
["COMMBAD"]="UPS_COMM_LOST 与 UPS 的通信已中断,请检查连接"
["COMMOK"]="UPS_COMM_RESTORED 与 UPS 的通信已恢复"
)
# 遍历所有事件类型
for EVENT_TYPE in "${!EVENT_CONFIG[@]}"; do
IFS=' ' read -r -a config <<< "${EVENT_CONFIG[$EVENT_TYPE]}"
EVENT_ID="${config[0]}"
DEFAULT_MSG="${config[1]}"
# 提取最新一行含 EVENT_ID 的日志,并只保留 JSON 部分
LOG_LINE=$(grep -a "$EVENT_ID" "$SYSLOG_FILE" | tail -n1)
if [ -z "$LOG_LINE" ]; then
log "$EVENT_TYPE" "❌ 未发现事件关键词"
continue
fi
# 提取完整的 JSON 部分(从第一个 '{' 开始到最后)
JSON_PART=$(echo "$LOG_LINE" | grep -o '{.*}')
if [ -z "$JSON_PART" ] || ! echo "$JSON_PART" | jq . >/dev/null 2>&1; then
log "$EVENT_TYPE" "⚠️ JSON 提取失败或无效"
log "$EVENT_TYPE" "🔍 原始日志行: $LOG_LINE"
continue
fi
NEW_EVENT_ID=$(echo "$JSON_PART" | jq -r '.eventId')
NEW_EVENT_TIME=$(echo "$JSON_PART" | jq -r '.datetime')
if [ "$NEW_EVENT_ID" != "$EVENT_ID" ]; then
log "$EVENT_TYPE" "⚠️ eventId 不匹配: $NEW_EVENT_ID vs $EVENT_ID"
continue
fi
if [ -z "$NEW_EVENT_TIME" ] || [ "$NEW_EVENT_TIME" = "null" ]; then
log "$EVENT_TYPE" "⚠️ 无法提取时间戳"
continue
fi
STATE_FILE="$STATE_DIR/last_event_$EVENT_TYPE.timestamp"
if [ -f "$STATE_FILE" ]; then
LAST_EVENT_TIME=$(cat "$STATE_FILE")
else
LAST_EVENT_TIME=0
fi
# 加载最后清理时间戳(如果存在)
CLEANUP_TIME=0
TIMESTAMP_FILE="$STATE_DIR/last_cleanup.timestamp"
if [ -f "$TIMESTAMP_FILE" ]; then
CLEANUP_TIME=$(cat "$TIMESTAMP_FILE")
fi
# 如果事件时间早于最后一次清理时间,则跳过
if (( NEW_EVENT_TIME < CLEANUP_TIME )); then
log "$EVENT_TYPE" "⏰ 事件发生在清理时间之前,跳过重复通知"
continue
fi
if (( NEW_EVENT_TIME > LAST_EVENT_TIME )); then
EVENT_TIME=$(date -d "@$NEW_EVENT_TIME" "+%Y-%m-%d %H:%M:%S")
log "$EVENT_TYPE" "🔔 检测到新事件,时间戳: $NEW_EVENT_TIME ($EVENT_TIME)"
NOTIFYTYPE="$EVENT_TYPE" \
NOTIFYMSG="$DEFAULT_MSG" \
NOTIFYDATETIME="$NEW_EVENT_TIME" \
"$NOTIFY_SCRIPT"
if [ $? -eq 0 ]; then
log "$EVENT_TYPE" "✅ 成功发送通知"
echo "$NEW_EVENT_TIME" > "$STATE_FILE"
else
log "$EVENT_TYPE" "❌ 通知执行失败"
fi
else
log "$EVENT_TYPE" "⏳ 已处理过该事件,跳过重复通知"
fi
done
功能:
- **解析 **
/var/log/syslog
中的 JSON 格式事件;
- **提取 **
eventId
和 datetime
;
- 若为新事件(时间大于上次记录),调用通知脚本;
- 支持多种事件类型:ONBATT, LOWBATT, FSD, ONLINE, COMMBAD, COMMOK;
- 忽略清理前发生的事件(避免误报);
示例日志输出:
2025-06-30 18:40:00 [ONBATT] 🔔 检测到新事件,时间戳: 1751213151 (2025-06-30 00:05:51)
2025-06-30 18:40:00 [ONBATT] ✅ 成功发送通知
3. 清理脚本:/usr/local/bin/cleanup_ups_logs.sh
#!/bin/bash
# ===== 配置区 =====
STATE_DIR="/tmp/ups_event_state"
LOG_FILES=(
"/var/log/ups_event_monitor.log"
"/var/log/nut/notifycmd.log"
"/var/log/ups_*_monitor.log"
)
TIMESTAMP_FILE="/tmp/ups_event_state/last_cleanup.timestamp"
# 清空所有日志文件内容
for LOG_FILE in "${LOG_FILES[@]}"; do
if [ -f "$LOG_FILE" ]; then
> "$LOG_FILE"
echo "✅ 已清空日志文件: $LOG_FILE"
fi
done
# 删除状态文件(下次启动会自动重建)
if [ -d "$STATE_DIR" ]; then
rm -f "$STATE_DIR"/*
echo "✅ 已清空状态目录: $STATE_DIR"
fi
# 记录当前时间为最后清理时间
echo "$(date +%s)" > "$TIMESTAMP_FILE"
echo "✅ 已记录清理时间戳: $(date '+%Y-%m-%d %H:%M:%S')"
功能:
- 清空所有相关日志文件内容;
- 删除状态文件(避免重复通知);
- 记录本次清理的时间戳,用于后续判断;
- 支持定时自动运行;
示例输出:
✅ 已清空日志文件: /var/log/ups_event_monitor.log
✅ 已清空日志文件: /var/log/nut/notifycmd.log
✅ 已清空状态目录: /tmp/ups_event_state
✅ 已记录清理时间戳: 2025-06-30 00:00:00
四、定时任务设置
设置每天凌晨 2 点运行监控和清理脚本:
# 添加定时任务
(crontab -l 2>/dev/null; echo "*/5 * * * * /usr/local/bin/ups_event_monitor.sh >> /var/log/ups_event_monitor_cron.log 2>&1") | crontab -
(crontab -l 2>/dev/null; echo "0 2 * * * /usr/local/bin/cleanup_ups_logs.sh >> /var/log/cleanup_ups.log 2>&1") | crontab -
# 查看定时任务
crontab -l
五、常用命令汇总
操作 |
命令 |
手动运行监控脚本 |
/usr/local/bin/ups_event_monitor.sh |
手动运行清理脚本 |
/usr/local/bin/cleanup_ups_logs.sh |
查看监控日志 |
tail -f /var/log/ups_event_monitor.log |
查看通知日志 |
tail -f /var/log/nut/notifycmd.log |
查看定时任务 |
crontab -l |
设置脚本可执行权限 |
chmod +x /usr/local/bin/ups_event_monitor.sh chmod +x /etc/nut/notifycmd chmod +x /usr/local/bin/cleanup_ups_logs.sh |
六、注意事项
注意项 |
说明 |
✅ 确保安装依赖 |
jq ,msmtp ,cron |
✅ 不要删除 syslog 文件 |
否则会导致事件无法读取 |
✅ 清理脚本必须包含状态文件清理 |
否则会重复通知旧事件 |
✅ 修改脚本后务必测试运行 |
确保无语法错误 |
✅ 邮件测试可用手动触发 |
NOTIFYTYPE=ONBATT NOTIFYMSG="test" UPSNAME="ups1" /etc/nut/notifycmd |