Power loss notification (UPS)
When the power goes out, you have exactly as much time as the UPS holds — usually a few minutes. That’s enough to shut the server down gracefully by hand, or at least to understand that everything is about to power off. A push with the current battery charge and remaining runtime lets you react while there is still some reserve left. Sudden power loss often correlates with unexpected reboots — see unexpected reboot.
What we’ll do
Section titled “What we’ll do”The script polls the UPS via NUT (upsc) every minute and looks at the
ups.status field:
OL— on line power (everything is fine);OB/ONBATT— the server is running on battery (power lost);LB— low charge, shutdown is imminent.
Notification logic:
- switch to battery — a high-priority (9) message 🔌;
- low battery charge — priority 10 🔋, the loudest alert;
- return to line power — a calm recovery message, priority 4 ✅.
In the message we show the battery charge (battery.charge) and the estimated
runtime (battery.runtime). To avoid receiving the same thing every minute, we
store the state (ok / onbatt / lowbatt) in /var/lib/notifly-ups/.
Script
Section titled “Script”This assumes NUT is already configured and upsc <ups_name>@localhost returns
data. You can find the UPS name with upsc -l. Save as
/usr/local/bin/notifly-ups-check:
#!/usr/bin/env bashset -euset -a; source /etc/notifly.env; set +a
UPS="${1:-ups@localhost}" # UPS name, see `upsc -l`STATE_DIR=/var/lib/notifly-upsmkdir -p "$STATE_DIR"STATE_FILE="$STATE_DIR/state"
HOST=$(hostname -s)
# Read the current UPS readingsSTATUS=$(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 comes in seconds — convert to minutes for humansif [ -n "$RUNTIME" ]; then MINS=$(( RUNTIME / 60 )) RUNLEFT="${MINS} мин"else RUNLEFT="н/д"fi
# Determine the level from the flags in ups.statusLEVEL=okcase " $STATUS " in *" OB "*|*" ONBATT "*) LEVEL=onbatt ;;esaccase " $STATUS " in *" LB "*) LEVEL=lowbatt ;; # low charge takes priority over 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"fiMake it executable:
sudo chmod +x /usr/local/bin/notifly-ups-checkAlternative: apcupsd
Section titled “Alternative: apcupsd”If you use apcupsd instead of NUT, the data is read with apcaccess status. The
STATUS field contains ONLINE or ONBATT, BCHARGE is the charge in percent,
and TIMELEFT is the remaining time. A variant of the script:
#!/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 returns "KEY : value" — pull out the fields we needSTAT=$(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 reports the low-charge flag via "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"fiRun on schedule
Section titled “Run on schedule”Power needs to be checked more often than disk — once a minute, so as not to miss a short UPS discharge.
Using cron
Section titled “Using cron”sudo crontab -eAdd the line (pass your UPS name if it isn’t ups):
* * * * * /usr/local/bin/notifly-ups-check ups@localhost >/dev/null 2>&1Using systemd timer (recommended)
Section titled “Using systemd timer (recommended)”/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.targetActivate it:
sudo systemctl daemon-reloadsudo systemctl enable --now notifly-ups.timersudo systemctl list-timers notifly-ups.timerWindows: PowerShell + Task Scheduler
Section titled “Windows: PowerShell + Task Scheduler”Many consumer UPS units on Windows have no daemon of their own, but expose the
battery charge to the operating system. We read Win32_Battery via CIM: the
BatteryStatus field (1 = on battery / discharging, 2 = on AC power),
EstimatedChargeRemaining is the charge in percent, and EstimatedRunTime is
the remaining time in minutes. We use the common function Send-Notifly from the
sysadmin template/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 } # no battery/UPS — exit
$charge = $bat.EstimatedChargeRemaining$runtime = $bat.EstimatedRunTime # minutes; 71582788 = "not calculated"$runLeft = if ($runtime -and $runtime -lt 1000000) { "$runtime мин" } else { "н/д" }
# BatteryStatus: 1 = discharging (on battery), 2 = on AC power$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}Register in Task Scheduler (as administrator, every minute as 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: контроль питания ИБП"Benefits
Section titled “Benefits”- You have time to shut the server down by hand — the push arrives the moment it switches to battery, while you still have a few minutes of reserve, not after a hard power-off.
- You see the remaining battery right in the notification — the charge and the estimated runtime immediately tell you how much time you have.
- Three levels without spam (
ok→onbatt→lowbatt): as long as the state doesn’t change, there are no notifications; a separate loud alert (priority 10) on low charge won’t let you sleep through the shutdown.
What to improve next
Section titled “What to improve next”- Add priority 10 with a repeating sound specifically for the low-charge event — to wake the on-call engineer.
- Attach the full output of
upsc ups@localhost(voltage, load, temperature) via theextrasfield. - Tie it together with the unexpected reboot detector: if the server did reboot after the power loss, you’ll get both stories.