A UPS is insurance you buy once and then stop thinking about, which is exactly how it fails. The battery ages until it holds ninety seconds instead of twenty minutes. The USB cable works loose and the host, which was supposed to shut down cleanly when the battery ran low, never hears a word from the UPS. The weekly self-test that would have caught both silently stops. Then the power actually goes out, and the first you learn of any of it is a NAS with a corrupted filesystem.
Both common UPS daemons already know everything you'd want to be told. NUT (Network UPS Tools) and apcupsd poll the UPS constantly and expose its status, charge, runtime, and diagnostic flags. What they don't do is tell you, from outside the house, in a way that survives the outage. This guide puts a one-minute heartbeat on top of that status so a power cut, a dying battery, or a disconnected UPS becomes an alert on your phone, and covers the Synology-as-NUT-server setup, Home Assistant, and the self-test and battery-age tracking that keep the numbers honest.
What the daemon knows
With NUT, upsc <upsname>@localhost prints the UPS's variables. The ones that matter:
ups.status: a space-separated set of flags.OLis on line power,OBon battery,LBlow battery,RBreplace battery,CHRGandDISCHRGcharging state,OVERoverload,BYPASS, andFSDforced shutdown. Healthy isOLorOL CHRG.battery.charge(percent) andbattery.runtime(seconds remaining at the current load).ups.load(percent of capacity) andinput.voltage, useful for spotting an overloaded UPS or brownouts.- An error instead of variables,
Error: Data stale, means the driver has lost contact with the UPS: the cable, the USB port, or the driver process. This is the failure that turns a UPS into a very heavy power strip.
With apcupsd, apcaccess status prints the equivalent: STATUS (ONLINE, ONBATT, LOWBATT, REPLACEBATT, COMMLOST), BCHARGE, TIMELEFT (minutes), LOADPCT, and SELFTEST.
The heartbeat script
Create a heartbeat monitor in CronAlert with a 1-minute expected interval; with the default grace it alerts after two minutes of silence, which is about as fast as you want to hear about a power cut. Then install this on the host that talks to the UPS:
#!/usr/bin/env bash
# /usr/local/bin/ups-health.sh — run from cron every minute
set -uo pipefail
HEARTBEAT="https://cronalert.com/api/heartbeat/<token>"
UPS="ups@localhost" # NUT name; use "[email protected]" for a NAS acting as NUT server
MIN_RUNTIME=600 # seconds: your shutdown margin plus a buffer
fail=0
if command -v upsc >/dev/null; then
if ! vars=$(upsc "$UPS" 2>&1); then
echo "ups unreachable: $vars"; fail=1 # includes "Data stale" — the UPS is disconnected
else
status=$(echo "$vars" | awk -F': ' '/^ups.status/ {print $2}')
charge=$(echo "$vars" | awk -F': ' '/^battery.charge:/ {print $2}')
runtime=$(echo "$vars" | awk -F': ' '/^battery.runtime:/ {print $2}')
echo "$status" | grep -qw OL || { echo "not on line power: $status"; fail=1; }
echo "$status" | grep -qwE 'LB|RB|OVER|FSD' && { echo "ups flag: $status"; fail=1; }
[ -n "${runtime:-}" ] && [ "$runtime" -lt "$MIN_RUNTIME" ] && { echo "runtime ${runtime}s below ${MIN_RUNTIME}s"; fail=1; }
[ -n "${charge:-}" ] && [ "$charge" -lt 90 ] && echo "$status" | grep -qvw CHRG && { echo "battery at ${charge}% while not charging"; fail=1; }
fi
elif command -v apcaccess >/dev/null; then
if ! vars=$(apcaccess status 2>&1); then
echo "apcupsd unreachable"; fail=1
else
status=$(echo "$vars" | awk -F': ' '/^STATUS/ {print $2}')
left=$(echo "$vars" | awk -F': ' '/^TIMELEFT/ {print int($2)}')
echo "$status" | grep -qw ONLINE || { echo "not online: $status"; fail=1; }
echo "$status" | grep -qwE 'LOWBATT|REPLACEBATT|COMMLOST|OVERLOAD' && { echo "ups flag: $status"; fail=1; }
[ -n "${left:-}" ] && [ $((left * 60)) -lt "$MIN_RUNTIME" ] && { echo "runtime ${left}m too short"; fail=1; }
fi
fi
# Ping only when the UPS is on line power and healthy; silence is the alert
[ "$fail" -eq 0 ] && curl -fsS -m 10 -X POST "$HEARTBEAT" >/dev/null
exit 0 # /etc/cron.d/ups-health
* * * * * root /usr/local/bin/ups-health.sh 2>&1 | logger -t ups-health Each line encodes a different failure. Anything other than OL is a power event. RB is the UPS's own verdict on its battery. A runtime below your shutdown margin means the battery can no longer cover a clean shutdown, whether through age or because you added load. A charge below 90 percent while not charging means the battery isn't holding what it's given. And an unreachable UPS, the quiet one, means the host will never be told to shut down when it matters. When the heartbeat goes silent, journalctl -t ups-health -n 20 says which.
Name the monitor for what silence means: "UPS: on battery, low runtime, or disconnected". Heartbeats are on the Pro plan and above.
What the on-battery alert actually tells you
Here's the subtlety. The script pings over the internet, and during a power cut the internet is only up if the modem and router are on battery too. If they aren't, the heartbeat stops because it can't be sent, not because the script decided to withhold it, and CronAlert alerts you that the host went silent. That's still correct, just less specific: "something at home is off" rather than "on battery, 18 minutes of runtime left."
The fix is cheap: a modem and router draw a few watts, so put them on the UPS. Then during an outage the host keeps reporting, the script's log shows runtime ticking down minute by minute, and if you have a second heartbeat on the NAS or the Proxmox node on the same UPS, you'll see their clean shutdowns as their heartbeats stop in sequence. Either way, a heartbeat is the right monitor shape for a UPS: its failure mode during the event you care about is silence, and silence is the alert.
A NAS as the NUT server
Many homelabs have the UPS plugged into a Synology or QNAP, which runs its own NUT server. Synology's is under Control Panel → Hardware & Power → UPS with Enable network UPS server and a list of permitted client IPs; the UPS is named ups and the client credentials are the NUT defaults (monuser / secret). Any other host on the LAN can then run the script above with UPS="ups@<nas-ip>" after installing nut-client, and upsmon on that host will also shut it down when the NAS declares low battery. The NAS's own UPS status is already part of the NAS heartbeat script if you add one line to it: upsc ups@localhost ups.status | grep -qw OL.
Home Assistant and the UPS
If you run Home Assistant, its NUT integration exposes the UPS as entities (sensor.ups_status, sensor.ups_battery_charge, sensor.ups_battery_runtime). Add a condition to the heartbeat automation from that guide requiring sensor.ups_status to contain Online (or OL, depending on version), and your existing Home Assistant heartbeat now also goes quiet when the house is on battery, with no extra script. Use one or the other, not both, unless you want two alerts for one power cut.
Self-tests and battery age
The runtime and charge numbers are only as good as the UPS's last calibration, and a battery that hasn't been exercised reports optimistic figures. Run the UPS's quick self-test monthly: with NUT, upscmd -u admin -p <pw> ups test.battery.start.quick from a cron entry or a systemd timer; apcupsd runs a self-test weekly by default and records the result in SELFTEST (OK, BT for battery failure, NG). Add SELFTEST not equal to OK as a failure line if you use apcupsd. And write the battery's install date somewhere the script can read (NUT exposes battery.mfr.date or battery.date on some models; otherwise a file); fail the heartbeat, or at least log loudly, when it passes three to four years. A replacement battery costs a fraction of one unclean shutdown.
Frequently asked questions
Can CronAlert see my UPS from outside?
No. The host reads the status from NUT or apcupsd and reports it through a heartbeat.
How does the alert get out during a power cut?
Only if the modem and router are on the UPS. Otherwise the heartbeat stops because it can't be sent, which still alerts you, just less specifically.
How do I catch a dying battery early?
Fail on the RB flag, on runtime below your shutdown margin, and on charge below 90 percent while not charging. Run monthly self-tests.
Does this work with a Synology as the NUT server?
Yes. Enable the network UPS server on the NAS and point upsc at ups@<nas-ip> from any client.
Which plan?
Heartbeats are Pro ($5/mo).
Make the insurance report in
The UPS already measures everything that predicts its own failure. One script reading those numbers each minute, and a heartbeat that goes quiet the moment they're wrong, turns a power cut into a timeline on your phone and a dying battery into a replacement you schedule instead of a shutdown you don't. Create an account, upgrade to Pro, and add the heartbeat before the next storm. Related reading: monitoring a server end to end, monitoring a NAS, monitoring a Proxmox host, monitoring Pi-hole or AdGuard Home (which usually shares the UPS), and choosing between HTTP, TCP, and heartbeat monitors.