The UPS looked ordinary right up until NUT refused to see it the way I expected.

These Prolink 650VA and 850VA units do not behave like the USB HID UPS gear many people assume they are. Both units showed up as 0665:5161 USB-to-serial devices, and the shortest working path was to use nutdrv_qx, not usbhid-ups.

This post is the cleaned-up version of the setup I ended up with across two Linux hosts:

  • a Debian NUC in the PC room acting as a NUT server
  • a Proxmox node in the living room acting as a second NUT server and client endpoint for Synology DSM

I am keeping the example hostnames, IPs and tokens generic. Replace them with your own values.

What mattered first

The first useful discovery was simple:

  • both UPS units exposed as 0665:5161 INNO TECH/Cypress USB to Serial
  • nutdrv_qx was the driver that worked
  • usbhid-ups was the wrong choice for these units
  • on Proxmox, run the commands as root
  • on Debian, use sudo unless you are already root

Before you touch NUT config, make sure the host can actually see the device:

lsusb | grep 0665
nut-scanner -U

A working scan should look broadly like this:

[nutdev1]
    driver = "nutdrv_qx"
    port = "auto"
    vendorid = "0665"
    productid = "5161"
    product = "USB to Serial"
    vendor = "INNO TECH"

If you do not get something in that shape, stop there. Fix device detection before chasing NUT config.

Debian NUC Machine: NUT server for the PC room UPS

On the Debian host, install NUT first:

sudo apt update
sudo apt install -y nut

Set NUT to server mode:

sudo tee /etc/nut/nut.conf > /dev/null <<'EOF'
MODE=netserver
EOF

Define the UPS in /etc/nut/ups.conf:

sudo tee /etc/nut/ups.conf > /dev/null <<'EOF'
[pcroomups]
    driver = nutdrv_qx
    port = auto
    vendorid = 0665
    productid = 5161
    desc = "PC Room UPS"
EOF

Let upsd listen on the network:

sudo tee /etc/nut/upsd.conf > /dev/null <<'EOF'
LISTEN 0.0.0.0 3493
EOF

Create a local monitor account. Use a real password on your host, not the placeholder below:

sudo tee /etc/nut/upsd.users > /dev/null <<'EOF'
[localmon]
    password = CHANGE_THIS_LONG_RANDOM_PASSWORD
    upsmon primary
EOF

Then configure upsmon:

sudo tee /etc/nut/upsmon.conf > /dev/null <<'EOF'
MONITOR pcroomups@localhost 1 localmon CHANGE_THIS_LONG_RANDOM_PASSWORD primary
MINSUPPLIES 1
SHUTDOWNCMD "/sbin/shutdown -h now"
POWERDOWNFLAG /etc/killpower
NOTIFYCMD /etc/nut/notify.sh

NOTIFYFLAG ONLINE   SYSLOG
NOTIFYFLAG ONBATT   SYSLOG+EXEC
NOTIFYFLAG LOWBATT  SYSLOG+EXEC
NOTIFYFLAG FSD      SYSLOG+EXEC
NOTIFYFLAG SHUTDOWN SYSLOG+EXEC
EOF

Telegram notifications

I kept Telegram credentials outside the script so the main file stays clean.

sudo tee /etc/nut/telegram.env > /dev/null <<'EOF'
TELEGRAM_BOT_TOKEN="YOUR_BOT_TOKEN"
TELEGRAM_CHAT_ID="YOUR_CHAT_ID"
EOF

sudo chown root:nut /etc/nut/telegram.env
sudo chmod 640 /etc/nut/telegram.env

Notification script

This script handles the useful states: power restored, on battery, low battery, forced shutdown and communication events.

It also includes a remote shutdown hook for another Linux box. The Windows path was later disabled in my setup because that desktop was no longer on this UPS.

sudo tee /etc/nut/notify.sh > /dev/null <<'EOF'
#!/usr/bin/env bash
set -u

LOG_TAG="nut-pcroom"
TELEGRAM_ENV="/etc/nut/telegram.env"

REMOTE_HOST_IP="192.0.2.10"
REMOTE_SSH_TARGET="[email protected]"
REMOTE_SHUTDOWN_CMD="/usr/local/bin/shutdown-safe.sh --execute"

load_telegram_config() {
    if [ -r "$TELEGRAM_ENV" ]; then
        # shellcheck disable=SC1090
        . "$TELEGRAM_ENV"
    fi
}

log_msg() {
    logger -t "$LOG_TAG" "$1"
}

send_telegram() {
    local text="$1"

    load_telegram_config

    if [ -z "${TELEGRAM_BOT_TOKEN:-}" ] || [ -z "${TELEGRAM_CHAT_ID:-}" ]; then
        log_msg "Telegram config missing; message not sent: $text"
        return 0
    fi

    curl -fsS \
        --connect-timeout 5 \
        --max-time 15 \
        --data-urlencode "chat_id=${TELEGRAM_CHAT_ID}" \
        --data-urlencode "text=${text}" \
        "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
        >/dev/null 2>&1 || log_msg "Telegram send failed: $text"
}

is_reachable() {
    local ip="$1"
    ping -c1 -W2 "$ip" >/dev/null 2>&1
}

shutdown_remote_host_if_awake() {
    if is_reachable "$REMOTE_HOST_IP"; then
        log_msg "Remote host reachable; sending shutdown command"
        send_telegram "🛑 PC Room UPS: shutting down remote host."

        ssh \
            -o BatchMode=yes \
            -o ConnectTimeout=8 \
            -o StrictHostKeyChecking=no \
            "$REMOTE_SSH_TARGET" \
            "$REMOTE_SHUTDOWN_CMD" &
    else
        log_msg "Remote host not reachable; skipping"
        send_telegram "â„šī¸ PC Room UPS: remote host not reachable, skipping shutdown."
    fi
}

shutdown_self() {
    log_msg "Self shutdown: NUC shutting down now"
    send_telegram "đŸ”ģ PC Room UPS: NUC is shutting itself down now."

    sleep 2
    /sbin/shutdown -h now
}

notify_type="${NOTIFYTYPE:-UNKNOWN}"
log_msg "NUT event: ${notify_type}"

case "$notify_type" in
    ONLINE)
        log_msg "Power restored"
        send_telegram "✅ PC Room power restored. UPS is back online."
        ;;

    ONBATT)
        log_msg "UPS on battery"
        send_telegram "⚡ PC Room UPS is on battery. Monitoring for low battery."
        ;;

    LOWBATT|FSD)
        log_msg "Critical UPS event ${notify_type}; initiating shutdown sequence"
        send_telegram "🔋 PC Room UPS critical event: ${notify_type}. Starting shutdown sequence."

        shutdown_remote_host_if_awake

        wait

        send_telegram "✅ PC Room UPS: remote shutdown commands completed. NUC will shut down last."
        shutdown_self
        ;;

    SHUTDOWN)
        log_msg "NUT SHUTDOWN event received"
        send_telegram "âš ī¸ PC Room UPS: NUT SHUTDOWN event received."
        ;;

    COMMBAD)
        log_msg "UPS communication lost"
        send_telegram "âš ī¸ PC Room UPS communication lost."
        ;;

    COMMOK)
        log_msg "UPS communication restored"
        send_telegram "✅ PC Room UPS communication restored."
        ;;

    *)
        log_msg "Unhandled NUT event: ${notify_type}"
        send_telegram "â„šī¸ PC Room UPS event: ${notify_type}"
        ;;
esac

exit 0
EOF

sudo chown root:nut /etc/nut/notify.sh
sudo chmod 750 /etc/nut/notify.sh

Set the permissions and start the services:

sudo chown root:nut /etc/nut/*.conf /etc/nut/upsd.users
sudo chmod 640 /etc/nut/*.conf /etc/nut/upsd.users
sudo install -d -m 770 -o nut -g nut /run/nut
sudo systemctl restart nut-server nut-monitor
sudo systemctl enable nut-server nut-monitor

Verify the Debian host

Check that NUT is responding:

upsc pcroomups@localhost

A healthy result should include values in this ballpark:

driver.name: nutdrv_qx
ups.status: OL
battery.charge: 100

Proxmox node: second NUT server for the living room UPS

On the Proxmox node, run the commands as root.

Install NUT:

apt update
apt install -y nut

Set NUT to server mode:

tee /etc/nut/nut.conf > /dev/null <<'EOF'
MODE=netserver
EOF

Use ups as the UPS name. That keeps the naming compatible with Synology DSM expectations in my setup.

tee /etc/nut/ups.conf > /dev/null <<'EOF'
[ups]
    driver = nutdrv_qx
    port = auto
    vendorid = 0665
    productid = 5161
    desc = "Living Room UPS"
EOF

Allow upsd to listen on the LAN:

tee /etc/nut/upsd.conf > /dev/null <<'EOF'
LISTEN 0.0.0.0 3493
EOF

Create a local monitor account plus a client account for DSM:

tee /etc/nut/upsd.users > /dev/null <<'EOF'
[localmon]
    password = CHANGE_THIS_LONG_RANDOM_PASSWORD
    upsmon primary

[monuser]
    password = secret
    upsmon secondary
EOF

If DSM has trouble connecting, the client role may need adjustment on older versions. In my notes, upsmon slave was the fallback to try.

Configure upsmon:

tee /etc/nut/upsmon.conf > /dev/null <<'EOF'
MONITOR ups@localhost 1 localmon CHANGE_THIS_LONG_RANDOM_PASSWORD primary

MINSUPPLIES 1
SHUTDOWNCMD "/sbin/shutdown -h now"
POWERDOWNFLAG /etc/killpower

POLLFREQ 5
POLLFREQALERT 5
HOSTSYNC 30
DEADTIME 20
FINALDELAY 5

NOTIFYCMD /usr/sbin/upssched

NOTIFYFLAG ONLINE   SYSLOG+EXEC
NOTIFYFLAG ONBATT   SYSLOG+EXEC
NOTIFYFLAG LOWBATT  SYSLOG+EXEC
NOTIFYFLAG FSD      SYSLOG+EXEC
NOTIFYFLAG SHUTDOWN SYSLOG+EXEC
EOF

Set up upssched so the node can wait for a bit before forcing shutdown:

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

AT ONBATT * EXECUTE onbatt-alert
AT ONLINE * EXECUTE power-restored
AT ONBATT * START-TIMER onbatt-shutdown 300
AT ONLINE * CANCEL-TIMER onbatt-shutdown power-restored
AT LOWBATT * EXECUTE lowbatt-shutdown
AT FSD * EXECUTE fsd-shutdown
EOF

Then add the scheduler command script:

tee /usr/local/sbin/nut-upssched-cmd.sh > /dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

LOG_TAG="nut-upssched-node02"
BOT_TOKEN="YOUR_BOT_TOKEN"
CHAT_ID="YOUR_CHAT_ID"

send_telegram() {
    local text="$1"
    curl -fsS \
        --data-urlencode "chat_id=${CHAT_ID}" \
        --data-urlencode "text=${text}" \
        "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
        >/dev/null 2>&1 || logger -t "$LOG_TAG" "Telegram send failed"
}

log() { logger -t "$LOG_TAG" "$1"; }

force_nut_shutdown() {
    log "Triggering NUT forced shutdown state on Proxmox node."
    send_telegram "🔋 Living Room UPS shutdown triggered. Synology should enter safe mode; this node will shut down last."
    /sbin/upsmon -c fsd
}

case "${1:-unknown}" in
    onbatt-alert)
        log "Living Room UPS is on battery."
        send_telegram "⚡ Living Room UPS is on battery."
        ;;
    power-restored)
        log "Living Room power restored."
        send_telegram "✅ Living Room power restored. UPS shutdown timer cancelled."
        ;;
    onbatt-shutdown|lowbatt-shutdown)
        log "Living Room UPS shutdown event: ${1}"
        force_nut_shutdown
        ;;
    fsd-shutdown)
        log "Living Room UPS entered FSD state."
        ;;
    *)
        log "Unhandled upssched event: ${1:-unknown}"
        ;;
esac
EOF

chmod 750 /usr/local/sbin/nut-upssched-cmd.sh
chown root:nut /usr/local/sbin/nut-upssched-cmd.sh

Set permissions and start the services:

chown root:nut /etc/nut/*.conf /etc/nut/upsd.users
chmod 640 /etc/nut/*.conf /etc/nut/upsd.users
install -d -m 770 -o nut -g nut /run/nut
systemctl restart nut-server nut-monitor
systemctl enable nut-server nut-monitor

Verify the Proxmox node

upsc ups@localhost
upsc [email protected]
ss -tlnp | grep 3493

You want to see the usual signs of life:

driver.name: nutdrv_qx
ups.status: OL
battery.charge: 100
LISTEN ... 0.0.0.0:3493

If you get Driver not connected

This was the annoying part that tends to waste time. If the driver fails to connect, use a recovery sequence on the affected host.

Run this as root, or prefix with sudo on Debian:

systemctl stop nut-monitor nut-server
pkill -9 -f nutdrv_qx || true
pkill -9 -f usbhid-ups || true
pkill -9 -f upsdrvctl || true
find /run/nut -mindepth 1 ! -type d -delete
install -d -m 770 -o nut -g nut /run/nut
upsdrvctl -u nut start
ls -la /run/nut
systemctl start nut-server
sleep 2
systemctl start nut-monitor
upsc <ups-name>@localhost

The socket should end up owned by nut:nut.

srw-rw---- nut nut nutdrv_qx-<ups-name>

Synology DSM as a NUT client

On DSM, the UPS settings live under:

Control Panel -> Hardware & Power -> UPS

Choose the Synology UPS Server option and point it at the Proxmox node.

The client side in my notes used a standard NUT account like this:

UPS name: ups
Username: monuser
Password: secret

DSM may not show explicit username and password fields in every version. If it connects, you should see something like:

Status: Connected
Battery Charged: 100%
Manufacturer: Generic USB to Serial
Model: UPS

Safe tests first

If you only want to verify alert handling, run the non-destructive paths:

/usr/local/sbin/nut-upssched-cmd.sh onbatt-alert
/usr/local/sbin/nut-upssched-cmd.sh power-restored

Do not run the shutdown paths unless you are prepared for the machine to power off:

/usr/local/sbin/nut-upssched-cmd.sh onbatt-shutdown
/usr/local/sbin/nut-upssched-cmd.sh lowbatt-shutdown
upsmon -c fsd

Real power test

For an actual battery test, unplug the UPS from wall power. Do not pull the USB cable if your goal is to confirm battery behavior.

You should see the status switch to battery:

upsc <ups>@localhost | grep ups.status

Expected:

ups.status: OB

When utility power comes back, the UPS should return to online state:

ups.status: OL

One last practical warning: UPS load matters

The PC room UPS started complaining when I launched a Steam game, even though there was no power cut. That is usually overload or something very close to it, not a mysterious software bug.

Watch the live load from the server:

watch -n 1 'upsc pcroomups@localhost | grep -E "ups.status|ups.load|battery.charge|input.voltage|output.voltage"'

Warning signs include:

ups.load: 80+
ups.status: OL OVER
ups.status includes OVER
frequent audible alarm while input voltage remains normal

In practice, the 850VA unit is best reserved for lighter loads. Keep the NUC and only the devices that truly need clean shutdown protection on battery. Gaming PCs, gaming monitors and other high-draw accessories are often better off on surge-only protection unless the UPS rating is comfortably above the real load.

If you need to protect a gaming machine under real GPU load, aim for something closer to a 1500VA / 900W class UPS rather than hoping a small unit will quietly absorb an RTX 3080 Ti and its transient spikes. That hope usually lasts until the first loud beep.

Takeaway

The shortest working path for these Prolink USB UPS units was not fancy:

  • identify the device correctly as USB-to-serial
  • use nutdrv_qx
  • make NUT listen on the network where needed
  • keep notifications and shutdown logic in separate scripts
  • verify with upsc before trusting any shutdown automation

That was enough to turn a confusing USB device into something predictable and useful.