# Fix Prolink USB UPS Detection in NUT on Two Linux Hosts

A practical NUT setup for Prolink USB UPS units that appear as USB-to-serial devices on two different Linux hosts.

Author: Ruhani Rabin
Canonical: https://rabin.blog/fix-prolink-usb-ups-detection-in-nut-on-two-linux-hosts/
Published: 2026-08-05T03:40:00.000Z
Updated: 2026-10-11T16:32:33.000Z

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:

```bash
lsusb | grep 0665
nut-scanner -U
```

A working scan should look broadly like this:

```ini
[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:

```bash
sudo apt update
sudo apt install -y nut
```

Set NUT to server mode:

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

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

```bash
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:

```bash
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:

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

Then configure `upsmon`:

```bash
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.

```bash
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.

```bash
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="root@192.0.2.10"
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:

```bash
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:

```bash
upsc pcroomups@localhost
```

A healthy result should include values in this ballpark:

```text
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`.

Before changing the host’s shutdown configuration, [keep the Proxmox configuration under version control](https://rabin.blog/proxmox-config-backup-git-version-control-pve-nodes/) so the known-good state is recoverable.

Install NUT:

```bash
apt update
apt install -y nut
```

Set NUT to server mode:

```bash
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.

```bash
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:

```bash
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:

```bash
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`:

```bash
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:

```bash
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:

```bash
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:

```bash
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

```bash
upsc ups@localhost
upsc ups@192.0.2.20
ss -tlnp | grep 3493
```

You want to see the usual signs of life:

```text
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:

```bash
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`.

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

## Synology DSM as a NUT client

On DSM, the UPS settings live under:

```text
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:

```text
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:

```text
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:

```bash
/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:

```bash
/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:

```bash
upsc <ups>@localhost | grep ups.status
```

Expected:

```text
ups.status: OB
```

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

```text
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:

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

Warning signs include:

```text
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.
