Уведомление о пропаже питания (ИБП / UPS)
Когда пропадает электричество, у вас есть ровно столько времени, сколько держит ИБП — обычно несколько минут. Этого хватает, чтобы корректно погасить сервер вручную или хотя бы понять, что скоро всё выключится. Push с текущим зарядом батареи и оставшимся временем работы позволяет отреагировать, пока ещё есть запас. Часто внезапное отключение питания коррелирует с неожиданными перезагрузками — см. неожиданная перезагрузка.
Что мы будем делать
Заголовок раздела «Что мы будем делать»Скрипт каждую минуту опрашивает ИБП через NUT (upsc) и смотрит поле
ups.status:
OL— питание от сети (всё в норме);OB/ONBATT— сервер работает от батареи (пропало питание);LB— низкий заряд, скоро выключение.
Логика уведомлений:
- переход на батарею — сообщение с высоким приоритетом (9) 🔌;
- низкий заряд батареи — приоритет 10 🔋, самый громкий сигнал;
- возврат к питанию от сети — спокойное сообщение восстановления, приоритет 4 ✅.
В сообщении показываем заряд батареи (battery.charge) и расчётное время
работы (battery.runtime). Чтобы не получать одно и то же каждую минуту, храним
состояние (ok / onbatt / lowbatt) в /var/lib/notifly-ups/.
Предполагается, что NUT уже настроен и upsc <имя_ИБП>@localhost отдаёт данные.
Имя ИБП можно узнать командой upsc -l. Сохраните как
/usr/local/bin/notifly-ups-check:
#!/usr/bin/env bashset -euset -a; source /etc/notifly.env; set +a
UPS="${1:-ups@localhost}" # имя ИБП, см. `upsc -l`STATE_DIR=/var/lib/notifly-upsmkdir -p "$STATE_DIR"STATE_FILE="$STATE_DIR/state"
HOST=$(hostname -s)
# Читаем текущие показатели ИБПSTATUS=$(upsc "$UPS" ups.status 2>/dev/null || echo "UNKNOWN")CHARGE=$(upsc "$UPS" battery.charge 2>/dev/null || echo "?")RUNTIME=$(upsc "$UPS" battery.runtime 2>/dev/null || echo "")
# battery.runtime приходит в секундах — переводим в минуты для человекаif [ -n "$RUNTIME" ]; then MINS=$(( RUNTIME / 60 )) RUNLEFT="${MINS} мин"else RUNLEFT="н/д"fi
# Определяем уровень по флагам в ups.statusLEVEL=okcase " $STATUS " in *" OB "*|*" ONBATT "*) LEVEL=onbatt ;;esaccase " $STATUS " in *" LB "*) LEVEL=lowbatt ;; # низкий заряд имеет приоритет над onbattesac
LAST=$(cat "$STATE_FILE" 2>/dev/null || echo ok)
if [ "$LEVEL" != "$LAST" ]; then case "$LEVEL" in onbatt) /usr/local/bin/notifly-send \ "🔌 Пропало питание — $HOST" \ "Сервер перешёл на ИБП. Заряд $CHARGE%, хватит на $RUNLEFT." 9 ;; lowbatt) /usr/local/bin/notifly-send \ "🔋 Низкий заряд ИБП — $HOST" \ "Осталось $CHARGE% ($RUNLEFT). Скоро сервер выключится!" 10 ;; ok) /usr/local/bin/notifly-send \ "✅ Питание восстановлено — $HOST" \ "Сервер снова от сети. Заряд батареи $CHARGE%." 4 ;; esac echo "$LEVEL" > "$STATE_FILE"fiСделайте исполняемым:
sudo chmod +x /usr/local/bin/notifly-ups-checkАльтернатива: apcupsd
Заголовок раздела «Альтернатива: apcupsd»Если вы используете apcupsd вместо NUT, данные читаются командой
apcaccess status. Поле STATUS содержит ONLINE или ONBATT, BCHARGE —
заряд в процентах, TIMELEFT — оставшееся время. Вариант скрипта:
#!/usr/bin/env bashset -euset -a; source /etc/notifly.env; set +a
STATE_DIR=/var/lib/notifly-upsmkdir -p "$STATE_DIR"STATE_FILE="$STATE_DIR/state"
HOST=$(hostname -s)
# apcaccess отдаёт "КЛЮЧ : значение" — вытаскиваем нужные поляSTAT=$(apcaccess status 2>/dev/null || echo "")STATUS=$(echo "$STAT" | awk -F: '/^STATUS/ {gsub(/ /,"",$2); print $2}')CHARGE=$(echo "$STAT" | awk -F: '/^BCHARGE/ {gsub(/ /,"",$2); print $2+0}')RUNLEFT=$(echo "$STAT" | awk -F: '/^TIMELEFT/ {print $2}' | xargs)
LEVEL=okcase "$STATUS" in *ONBATT*) LEVEL=onbatt ;;esac# apcupsd отдаёт флаг низкого заряда через "STATUS: ONBATT LOWBATT"case "$STATUS" in *LOWBATT*) LEVEL=lowbatt ;;esac
LAST=$(cat "$STATE_FILE" 2>/dev/null || echo ok)
if [ "$LEVEL" != "$LAST" ]; then case "$LEVEL" in onbatt) /usr/local/bin/notifly-send \ "🔌 Пропало питание — $HOST" \ "Сервер перешёл на ИБП. Заряд $CHARGE%, хватит на $RUNLEFT." 9 ;; lowbatt) /usr/local/bin/notifly-send \ "🔋 Низкий заряд ИБП — $HOST" \ "Осталось $CHARGE% ($RUNLEFT). Скоро сервер выключится!" 10 ;; ok) /usr/local/bin/notifly-send \ "✅ Питание восстановлено — $HOST" \ "Сервер снова от сети. Заряд батареи $CHARGE%." 4 ;; esac echo "$LEVEL" > "$STATE_FILE"fiЗапуск по расписанию
Заголовок раздела «Запуск по расписанию»Питание нужно проверять чаще, чем диск, — раз в минуту, чтобы не пропустить короткий разряд ИБП.
Через cron
Заголовок раздела «Через cron»sudo crontab -eДобавьте строку (передайте имя вашего ИБП, если оно не ups):
* * * * * /usr/local/bin/notifly-ups-check ups@localhost >/dev/null 2>&1Через systemd timer (рекомендуется)
Заголовок раздела «Через systemd timer (рекомендуется)»/etc/systemd/system/notifly-ups.service:
[Unit]Description=Notifly UPS power check
[Service]Type=oneshotExecStart=/usr/local/bin/notifly-ups-check ups@localhost/etc/systemd/system/notifly-ups.timer:
[Unit]Description=Run notifly-ups every minute
[Timer]OnBootSec=1minOnUnitActiveSec=1min
[Install]WantedBy=timers.targetАктивируем:
sudo systemctl daemon-reloadsudo systemctl enable --now notifly-ups.timersudo systemctl list-timers notifly-ups.timerWindows: PowerShell + Task Scheduler
Заголовок раздела «Windows: PowerShell + Task Scheduler»Многие бытовые ИБП под Windows не имеют своего демона, но отдают заряд батареи
операционной системе. Читаем Win32_Battery через CIM: поле BatteryStatus
(1 = работа от батареи / разряд, 2 = питание от сети), EstimatedChargeRemaining
— заряд в процентах, EstimatedRunTime — оставшееся время в минутах. Используем
общую функцию Send-Notifly из
шаблона sysadmin/index.
. C:\scripts\Notifly.ps1
$StateDir = "C:\ProgramData\Notifly\ups-state"New-Item -ItemType Directory -Path $StateDir -Force | Out-Null$StateFile = Join-Path $StateDir "state.txt"$Host = $env:COMPUTERNAME
$bat = Get-CimInstance Win32_Battery | Select-Object -First 1if (-not $bat) { return } # нет батареи/ИБП — выходим
$charge = $bat.EstimatedChargeRemaining$runtime = $bat.EstimatedRunTime # минуты; 71582788 = "не рассчитано"$runLeft = if ($runtime -and $runtime -lt 1000000) { "$runtime мин" } else { "н/д" }
# BatteryStatus: 1 = разряд (на батарее), 2 = питание от сети (AC)$level = "ok"if ($bat.BatteryStatus -eq 1) { $level = "onbatt" }if ($charge -le 20 -and $bat.BatteryStatus -eq 1) { $level = "lowbatt" }
$last = if (Test-Path $StateFile) { Get-Content $StateFile -Raw } else { "ok" }
if ($level -ne $last.Trim()) { switch ($level) { "onbatt" { Send-Notifly -Title "🔌 Пропало питание — $Host" ` -Message "Сервер перешёл на ИБП. Заряд $charge%, хватит на $runLeft." -Priority 9 } "lowbatt" { Send-Notifly -Title "🔋 Низкий заряд ИБП — $Host" ` -Message "Осталось $charge% ($runLeft). Скоро сервер выключится!" -Priority 10 } "ok" { Send-Notifly -Title "✅ Питание восстановлено — $Host" ` -Message "Сервер снова от сети. Заряд батареи $charge%." -Priority 4 } } $level | Set-Content $StateFile}Регистрация в Task Scheduler (от администратора, каждую минуту от SYSTEM):
$Action = New-ScheduledTaskAction ` -Execute "powershell.exe" ` -Argument "-NoProfile -ExecutionPolicy Bypass -File C:\scripts\Notifly-UPS-Check.ps1"$Trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) ` -RepetitionInterval (New-TimeSpan -Minutes 1)$Princ = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel HighestRegister-ScheduledTask -TaskName "Notifly UPS Check" ` -Action $Action -Trigger $Trigger -Principal $Princ -Description "Notifly: контроль питания ИБП"- Успеваете погасить сервер вручную — push приходит в момент перехода на батарею, пока есть несколько минут запаса, а не после жёсткого выключения.
- Видите остаток батареи прямо в уведомлении — заряд и расчётное время работы сразу подсказывают, сколько у вас есть.
- Три уровня без спама (
ok→onbatt→lowbatt): пока состояние не меняется, уведомлений нет; отдельный громкий сигнал (приоритет 10) на низком заряде не даст проспать выключение.
Что улучшить дальше
Заголовок раздела «Что улучшить дальше»- Добавить приоритет 10 с повторяющимся звуком именно для события низкого заряда — чтобы разбудить дежурного.
- Приложить полный вывод
upsc ups@localhost(напряжение, нагрузка, температура) через полеextras. - Связать с детектором неожиданной перезагрузки: если после пропажи питания сервер всё же перезагрузился, вы получите обе истории.