Adding UPS Monitoring to a Linux Home Lab Server with NUT

I recently added a managed UPS to a Linux server in my lab and wanted the server to do more than just sit on battery backup. I wanted the server to detect the UPS, monitor the battery status, send email alerts, provide a web status page, and shut down cleanly if power was out for too long.

The UPS I used was a CyberPower CP1500PFCLCD PFC Sinewave UPS Battery Backup and Surge Protector, 1500VA/1000W, 12 Outlets, AVR, Mini Tower, UL Certified

Amazon product page:

https://www.amazon.com/CyberPower-CP1500PFCLCD-Sinewave-Outlets-Mini-Tower/dp/B00429N19W


This post walks through the setup in a lab environment using Linux, Network UPS Tools, systemd, Nginx, and a simple notification script.

Lab Environment


The lab server used for this setup was running:

Linux distribution: LMDE 7 “Gigi”
Base: Debian 13 “Trixie”
UPS: CyberPower CP1500PFCLCD
UPS connection: USB cable
UPS software: Network UPS Tools, also known as NUT
Web server: Nginx
Mail relay: Local sendmail-compatible relay


The examples below use sanitized names and addresses:

UPS name: lab-ups
Server IP: 192.168.1.5
Alert email: admin@example.com
Status page: http://192.168.1.5/status/ups/


Substitute your own values where needed.

What I Wanted the UPS Setup to Do


The goal was not just battery backup. I wanted useful management:

  • Detect the UPS over USB
  • Monitor line power, battery charge, runtime, and load
  • Send an email when power events happen
  • Wait before shutting down, in case power comes back quickly
  • Shut down cleanly if power stays out too long
  • Provide a web-based UPS status page
  • Start automatically after reboot


The final behavior looked like this:

  • Power goes out:
    • Start a 5-minute warning timer.
    • Start a 30-minute shutdown timer.
  • After 5 minutes on battery:
    • Send an email warning.
  • After 30 minutes on battery:
    • Send a shutdown warning.
    • Start clean shutdown.
  • If power comes back:
    • Cancel the timers.
    • Send a recovery email.
  • If the UPS reaches low battery:
    • Shut down immediately.


Step 1: Plug in the UPS USB Cable


First, I connected the UPS to the Linux server using the USB cable that came with the UPS.

Then I checked whether Linux could see it:

lsusb

The UPS appeared as a CyberPower USB device. Example:

Cyber Power System, Inc. CP1500PFCLCD UPS

I also checked recent kernel messages:

dmesg | tail -n 80

This confirmed that the server saw the USB device.

Step 2: Install Network UPS Tools


Network UPS Tools, or NUT, is the standard Linux toolset for monitoring UPS units.

Install it:

apt update
apt install -y nut nut-client nut-server

Then scan for USB UPS devices:

nut-scanner -U

The scan should return something similar to this:

[nutdev1]
    driver = "usbhid-ups"
    port = "auto"
    vendorid = "0764"
    productid = "0601"
    product = "CP1500PFCLCDa"
    vendor = "CPS"

The important line is:

driver = "usbhid-ups"

That is the driver used for many USB HID-compatible UPS units.

Step 3: Configure the UPS

Edit `/etc/nut/ups.conf`:

cp /etc/nut/ups.conf /etc/nut/ups.conf.bak.$(date +%Y%m%d-%H%M%S)

cat > /etc/nut/ups.conf <<'EOF'
[lab-ups]
    driver = usbhid-ups
    port = auto
    vendorid = 0764
    productid = 0601
    desc = "CyberPower CP1500PFCLCD UPS"
EOF

This defines the UPS as `lab-ups`.

Step 4: Set NUT to Standalone Mode

For a single server connected directly to one UPS, standalone mode is appropriate.

Configure `/etc/nut/nut.conf`:

cp /etc/nut/nut.conf /etc/nut/nut.conf.bak.$(date +%Y%m%d-%H%M%S)

cat > /etc/nut/nut.conf <<'EOF'
MODE=standalone
EOF

Step 5: Create the NUT Monitor User

NUT uses a local user account so `upsmon` can monitor the UPS through `upsd`.

Configure `/etc/nut/upsd.users`:

cp /etc/nut/upsd.users /etc/nut/upsd.users.bak.$(date +%Y%m%d-%H%M%S) 2>/dev/null || true

cat > /etc/nut/upsd.users <<'EOF'
[monuser]
    password = replace-this-with-a-strong-local-password
    upsmon master
EOF

chmod 640 /etc/nut/upsd.users
chown root:nut /etc/nut/upsd.users

Use a real password in your own environment.

Step 6: Configure UPS Monitoring

Configure `/etc/nut/upsmon.conf`:

cp /etc/nut/upsmon.conf /etc/nut/upsmon.conf.bak.$(date +%Y%m%d-%H%M%S)

cat > /etc/nut/upsmon.conf <<'EOF'
MONITOR lab-ups@localhost 1 monuser replace-this-with-a-strong-local-password master

MINSUPPLIES 1
SHUTDOWNCMD "/sbin/shutdown -h +0"
POLLFREQ 5
POLLFREQALERT 5
HOSTSYNC 15
DEADTIME 15
POWERDOWNFLAG /etc/killpower

NOTIFYFLAG ONLINE SYSLOG+WALL+EXEC
NOTIFYFLAG ONBATT SYSLOG+WALL+EXEC
NOTIFYFLAG LOWBATT SYSLOG+WALL+EXEC
NOTIFYFLAG FSD SYSLOG+WALL+EXEC
NOTIFYFLAG COMMOK SYSLOG+WALL+EXEC
NOTIFYFLAG COMMBAD SYSLOG+WALL+EXEC
NOTIFYFLAG SHUTDOWN SYSLOG+WALL+EXEC
NOTIFYFLAG REPLBATT SYSLOG+WALL+EXEC
NOTIFYFLAG NOCOMM SYSLOG+WALL+EXEC

NOTIFYCMD /usr/sbin/upssched
EOF

chmod 640 /etc/nut/upsmon.conf
chown root:nut /etc/nut/upsmon.conf

This tells `upsmon` to call `upssched`, which handles timed actions.

Step 7: Fix USB Permissions If Needed

On my setup, the UPS was detected but the driver initially could not open the USB device because of permissions.

The error looked like this:

libusb1: Could not open any HID devices: insufficient permissions on everything
No matching HID UPS found

The fix was to create a udev rule for the CyberPower UPS:

cat > /etc/udev/rules.d/62-cyberpower-ups.rules <<'EOF'
 CyberPower CP1500PFCLCD UPS for NUT
SUBSYSTEM=="usb", ATTR{idVendor}=="0764", ATTR{idProduct}=="0601", MODE="0660", GROUP="nut"
SUBSYSTEM=="usb_device", ATTR{idVendor}=="0764", ATTR{idProduct}=="0601", MODE="0660", GROUP="nut"
EOF

udevadm control --reload-rules
udevadm trigger

Then unplug the UPS USB cable, wait a few seconds, and plug it back in.

Check the permissions:

lsusb | grep -i -E 'cyber|power|ups|0764'

for dev in /dev/bus/usb/*/*; do
    if udevadm info -q property -n "$dev" 2>/dev/null | grep -q 'ID_VENDOR_ID=0764'; then
        echo "===== $dev ====="
        ls -l "$dev"
    fi
done


The device should be owned by root with group `nut`.

Step 8: Start the NUT Services

On Debian 13, the driver runs as a systemd instance, such as:

nut-driver@lab-ups.service

Start and enable the NUT services:

systemctl daemon-reload

systemctl enable --now nut-driver-enumerator.path
systemctl enable --now nut-driver-enumerator.service
systemctl enable --now nut-server
systemctl enable --now nut-monitor

Check the driver instance:

systemctl list-units 'nut-driver*' --all --no-pager

You should see something like:

nut-driver@lab-ups.service loaded active running

Then check all three main pieces:

systemctl is-active nut-driver@lab-ups.service nut-server nut-monitor

Expected:

active
active
active

Step 9: Query the UPS

Now query the UPS:

upsc lab-ups@localhost

Useful values include:

text
battery.charge
battery.runtime
input.voltage
output.voltage
ups.load
ups.status


Example:

text
battery.charge: 100
battery.runtime: 5525
input.voltage: 120.0
output.voltage: 120.0
ups.load: 7
ups.status: OL



`OL` means the UPS is on utility power.

Common status values:

text
OL        On line power
OB        On battery
LB        Low battery
OL CHRG   On line and charging


Step 10: Create an Email Notification Script

I wanted the server to send an email when UPS events happened.

Create `/usr/local/sbin/lab-ups-notify.sh`:

cat > /usr/local/sbin/lab-ups-notify.sh <<'EOF'
!/usr/bin/env bash

TO="admin@example.com"
FROM="Lab Server <server@example.com>"
LOG="/var/log/lab-ups.log"

EVENT="${NOTIFYTYPE:-UNKNOWN}"
MESSAGE="${*:-No message supplied}"
NOW="$(date '+%Y-%m-%d %H:%M:%S %Z')"
HOST="$(hostname -f 2>/dev/null || hostname)"

UPS_STATUS="$(upsc lab-ups@localhost 2>/dev/null || true)"

{
    echo "[$NOW] UPS event: $EVENT - $MESSAGE"
} >> "$LOG"

timeout 30 /usr/sbin/sendmail -t <<MAIL
From: ${FROM}
To: ${TO}
Subject: [LAB SERVER] UPS event: ${EVENT}

The lab server received a UPS event.

Time:
  ${NOW}

Host:
  ${HOST}

Event:
  ${EVENT}

Message:
  ${MESSAGE}

UPS Status:
${UPS_STATUS}
MAIL

exit 0
EOF

chmod 0755 /usr/local/sbin/lab-ups-notify.sh


Test it:

NOTIFYTYPE=TEST /usr/local/sbin/lab-ups-notify.sh "Manual UPS notification test"

If the email arrives, the notification script is working.

Step 11: Configure Timed Shutdown Behavior

I did not want the server to shut down immediately for a short power blink. I wanted it to wait.

The plan was:

  • After 5 minutes on battery:
    • Send warning email.
  • After 30 minutes on battery:
    • Start clean shutdown.
  • If power comes back:
    • Cancel shutdown timers and send recovery email.
  • If low battery happens:
    • Shut down immediately.

Create the `upssched` command script:

cat > /usr/local/sbin/lab-upssched-cmd.sh <<'EOF'
!/usr/bin/env bash

LOG="/var/log/lab-ups.log"
NOW="$(date '+%Y-%m-%d %H:%M:%S %Z')"

case "$1" in
    onbatt-warning)
        NOTIFYTYPE=ONBATT-WARNING /usr/local/sbin/lab-ups-notify.sh \
            "The lab server has been on UPS battery power for 5 minutes."
        echo "[$NOW] upssched: 5-minute on-battery warning sent" >> "$LOG"
        ;;

    onbatt-shutdown)
        NOTIFYTYPE=ONBATT-SHUTDOWN /usr/local/sbin/lab-ups-notify.sh \
            "The lab server has been on UPS battery power for 30 minutes. Starting clean shutdown."
        echo "[$NOW] upssched: 30-minute on-battery shutdown triggered" >> "$LOG"
        /sbin/upsmon -c fsd
        ;;

    online-recovery)
        NOTIFYTYPE=ONLINE-RECOVERY /usr/local/sbin/lab-ups-notify.sh \
            "Power has been restored. The lab server is back on utility power. UPS shutdown timers have been cancelled."
        echo "[$NOW] upssched: online recovery email sent; shutdown timers cancelled" >> "$LOG"
        ;;

    *)
        echo "[$NOW] upssched: unknown command: $1" >> "$LOG"
        ;;
esac
EOF

chmod 0755 /usr/local/sbin/lab-upssched-cmd.sh


Then configure `/etc/nut/upssched.conf`:

cp /etc/nut/upssched.conf /etc/nut/upssched.conf.bak.$(date +%Y%m%d-%H%M%S) 2>/dev/null || true

cat > /etc/nut/upssched.conf <<'EOF'
CMDSCRIPT /usr/local/sbin/lab-upssched-cmd.sh
PIPEFN /run/nut/upssched.pipe
LOCKFN /run/nut/upssched.lock

AT ONBATT * START-TIMER onbatt-warning 300
AT ONBATT * START-TIMER onbatt-shutdown 1800

AT ONLINE * CANCEL-TIMER onbatt-warning
AT ONLINE * CANCEL-TIMER onbatt-shutdown
AT ONLINE * EXECUTE online-recovery

AT LOWBATT * EXECUTE onbatt-shutdown
EOF

chmod 640 /etc/nut/upssched.conf
chown root:nut /etc/nut/upssched.conf

Restart the monitor:

systemctl restart nut-monitor

Verify:

systemctl status nut-monitor --no-pager

Step 12: Install the NUT CGI Web Status Page

I also wanted a simple web page to show UPS status.

Install the packages:

apt update
apt install -y nut-cgi fcgiwrap

Find the CGI files:

dpkg -L nut-cgi | grep -E 'cgi|upsstats|upsset|hosts'

On Debian, the CGI files are usually here:

/usr/lib/cgi-bin/nut/upsstats.cgi
/usr/lib/cgi-bin/nut/upsimage.cgi
/usr/lib/cgi-bin/nut/upsset.cgi

Configure `/etc/nut/hosts.conf`:

cp /etc/nut/hosts.conf /etc/nut/hosts.conf.bak.$(date +%Y%m%d-%H%M%S)

cat > /etc/nut/hosts.conf <<'EOF'
MONITOR lab-ups@localhost "Lab UPS"
EOF

Start `fcgiwrap`:

systemctl enable --now fcgiwrap.socket
systemctl status fcgiwrap.socket --no-pager

Step 13: Add an Nginx Location for the UPS Page

In my lab, I exposed the UPS page as a subpage of the server’s local status site:

http://192.168.1.5/status/ups/

The Nginx block looked like this:

location = /status/ups {
    return 301 /status/ups/;
}

location /status/ups/ {
    include fastcgi_params;
    fastcgi_pass unix:/run/fcgiwrap.socket;
    fastcgi_param SCRIPT_FILENAME /usr/lib/cgi-bin/nut/upsstats.cgi;
    fastcgi_param SCRIPT_NAME /status/ups/;
    fastcgi_param PATH_INFO "";
    fastcgi_param QUERY_STRING $query_string;
    add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0";
}

location = /status/ups/upsimage.cgi {
    include fastcgi_params;
    fastcgi_pass unix:/run/fcgiwrap.socket;
    fastcgi_param SCRIPT_FILENAME /usr/lib/cgi-bin/nut/upsimage.cgi;
    fastcgi_param SCRIPT_NAME /status/ups/upsimage.cgi;
    fastcgi_param QUERY_STRING $query_string;
    add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0";
}


Test Nginx:

nginx -t
systemctl reload nginx


Then test the page:

curl -I http://192.168.1.5/status/ups/

The detailed UPS page was available at:

http://192.168.1.5/status/ups/?host=lab-ups@localhost

Step 14: Final Verification Commands

Here are the main commands I use to check the UPS setup:

systemctl is-active nut-driver@lab-ups.service nut-server nut-monitor

upsc lab-ups@localhost | grep -E 'ups.status|battery.charge|battery.runtime|ups.load|input.voltage|output.voltage'

systemctl list-units 'nut-driver*' --all --no-pager

tail -n 50 /var/log/lab-ups.log

Expected UPS status on normal wall power:

ups.status: OL

Final Result

After setup, the server had:

  • UPS detected over USB
  • NUT driver running
  • NUT server running
  • NUT monitor running
  • Email alerts working
  • Timed shutdown behavior configured
  • Recovery email when power returns
  • Web status page available on the LAN
  • Automatic startup after reboot


At the current low load in my lab, the UPS reported roughly 90 minutes of runtime. I chose not to run the server all the way down. Instead, the system waits 30 minutes on battery, then shuts down cleanly unless power returns first.

That gives short outages a chance to clear while still protecting the server, filesystems, and running services from an uncontrolled power loss.

Closing Thoughts

A UPS is useful by itself, but it becomes much more useful when the server can talk to it. With a USB cable and Network UPS Tools, a Linux server can monitor power status, send alerts, display a web status page, and shut itself down cleanly.

For a home lab or small server rack, this is one of those upgrades that is worth doing before the next power outage.