Setting up NUT UPS Software on Linux (a journey of discovery)

Preamble

I've written about this UPS software numerous times. The coverage was normally a stage in a larger guide. I was asked by a friend to help him setup NUT (Network UPS Tools) on his Linux box and decided to feed my notes into an AI, prompting it to update them and make improvements.

NUT is an open-source solution distributed under the GNU General Public License (GPL). It is manufacture neutral and is heading towards its 30th anniversary. Another important observations is that it is able to support a wide array of devices spanning the evolution of the technology over the years. To this one needs to add the fact that there is no standards body to ensure that every brand follows a fixed API that had evolved with oversight over time.

The relevance of this information to the narrative is that the persons who support this project are volunteers who dedicate their own time to maintaining the project and do not have access to every UPS ever produced.

This means that if the UPS you are monitoring with NUT is a unit that is correctly detected by NUT scanner the process is straightforward. If it is not you have to put in a bit of elbow grease or work with the NUT volunteers to find the correct driver for your UPS.

The official site of NUT is https://networkupstools.org/.

The UPS I was working with is an older model. I’ve decided to document this journey of discovery in the hope that it guides you on what to look for—and how to resolve issues—when setting up NUT with your own hardware.

All the output is from the UPS I wanted to have monitored by NUT.

Below is the updated guide.

Parameters used in this document

This tutorial is based around a specific use case. Your values will be different. You may want to take this document and perform a find & replace to reflect your environment. Such a change will make it easier to copy and paste scripts across to your setup.

Parameter value Command / Comments
vendor_id 0665 lsusb and nut-scanner
productid 5161 lsusb and nut-scanner
serial ****0826 nut-scanner
port auto nut-scanner
driver nutdrv_qx nut-scanner
driver blazer_usb nut-scanner
ups name pve_ups multiple config files

upsmon monitoring account / multiple config files
upsmon upsmon_pass multiple config files

upsadmin admin account / multiple config files
updasmin updasmin_pass admin password

email@addr.com email address that will receive notifications

smtpemail@addr.com smtp sending email address

smtpemail_pass smtp app password

Hardware and Goals

OS: Debian 14.2.0

UPS Model: Powerwalker VI 2200 IEC/UK 

S/N: **** 0154  

Shutdown conditions: if on-battery for 5 minutes, or battery charge ≤ 40%; allow 5 minutes for OS shutdown before UPS power cut-off.


Installing NUT


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

This installs the NUT daemons and utilities (including the upsdrvctl, upsd, upsmon, upsc, etc.).

Identifying the UPS and Driver

  1. Plug in the UPS and run lsusb to find its USB IDs:

lsusb

The output shows e.g. Bus 001 Device 004: ID 0665:5161, where vendor_id=0665 and productid=5161.


Bus 001 Device 004: ID 0665:5161 Cypress Semiconductor USB to Serial
  1. Use nut-scanner to identify the correct driver and settings:

nut-scanner -U

Output:


Scanning USB bus.
[nutdev1]
        driver = "nutdrv_qx"
        port = "auto"
        vendorid = "0665"
        productid = "5161"
        product = "USB to Serial"
        serial = "****0826"
        vendor = "INNO TECH"
        bus = "001"
        device = "004"
        busport = "011"
        ###NOTMATCHED-YET###bcdDevice = "0002"

Configuring the UPS (ups.conf)

Edit /etc/nut/ups.conf and add a section for the UPS using the values above.


nano /etc/nut/ups.conf

Add the following block (using the values from nut-scanner):


# /etc/nut/ups.conf
# Global Directives: Define maximum retries and polling intervals
maxretry = 3
pollinterval = 2

[pve_ups]
    driver = nutdrv_qx
    port = auto
    vendorid = 0665
    productid = 5161
    desc = "INNO TECH USB UPS"
    # Set low battery threshold to 40%
    default.battery.charge.low = 40
    override.battery.charge.low = 40 
    # serial = (if needed)
    # Optional: override protocol if needed
    # protocol = megatec
  • maxretry: defines how many times NUT will try to start the UPS driver if the initial attempt fails.
  • pollinterval: defines how frequently (in seconds) the driver polls the UPS for status updates.
  • driver/port/vendorid/productid: as detected.
  • desc: human-readable name.
  • default.battery.charge.low: desired low-battery threshold (40%).
  • override.battery.charge.low: When a UPSs default.battery.charge.low is read only we are not able to change it. In such a case we also set override.battery.charge.low. This topic is further explored below.

Note: If the driver fails to start or can’t read the UPS and your UPS has protocol hints, try adding a protocol hint (the nutdrv_qx driver supports various protocols, e.g. protocol = megatec).

Test the driver:


upsdrvctl start

Output


Network UPS Tools - UPS driver controller 2.8.1
Network UPS Tools - Generic Q* USB/Serial driver 0.36 (2.8.1)
USB communication driver (libusb 1.0) 0.46
dstate_setflags: base variable (battery.charge.low) is immutable
Using protocol: Q1 0.08
Can't autodetect number of battery packs [-1/27.10]
Battery runtime will not be calculated (runtimecal not set)

Note the outputs discussed above:

  • base variable (battery.charge.low) is immutable message;
  • Battery runtime will not be calculated (runtimecal not set). If your UPS only reports the battery charge percentage (e.g., 80%) without telling the driver how many minutes that equates to. NUT can calculate this for you, but it needs a "map" to do the math. Since that map (the runtimecal variable) isn't in your config file, NUT is letting you know it won't be calculated.

Troubleshooting: Low Battery Threshold

Attempting to set battery.charge.low directly in the UPS causes an error (read-only). Instead, use the override parameter or the upsrw command. For example:


upsrw -s battery.charge.low=40 -u upsadmin -p upsadmin_pass pve_ups@localhost

NUT uses override.battery.charge.low as a software override of the UPS’s immutable built-in threshold. In other words, by default the UPS will signal “low battery” at its own hardware threshold, causing upsmon to shut down. Setting the override tells NUT to use the custom 40% level instead.

UPS parameters that can be changed

For a list of all the parameters that are read-write on a ups:


upsrw pve_ups@localhost

Verifying UPS Settings

List / Check values NUT sees:


upsc pve_ups@localhost

Output:


Init SSL without certificate database
battery.charge.low: 40
battery.voltage: 27.1
device.type: ups
driver.debug: 0
driver.flag.allow_killpower: 0
driver.name: nutdrv_qx
driver.parameter.default.battery.charge.low: 40
driver.parameter.override.battery.charge.low: 40
driver.parameter.pollfreq: 30
driver.parameter.pollinterval: 2
driver.parameter.port: auto
driver.parameter.productid: 5161
driver.parameter.synchronous: auto
driver.parameter.vendorid: 0665
driver.state: quiet
driver.version: 2.8.1
driver.version.data: Q1 0.08
driver.version.internal: 0.36
driver.version.usb: libusb-1.0.28 (API: 0x100010a)
input.frequency: 49.6
input.voltage: 235.2
input.voltage.fault: 235.2
output.voltage: 234.7
ups.beeper.status: enabled
ups.delay.shutdown: 30
ups.delay.start: 180
ups.load: 7
ups.productid: 5161
ups.status: OL
ups.type: offline / line interactive
ups.vendorid: 0665

 

Missing remaining charge

When inspecting the output I noticed that there was no charge parameter. If the driver could not read the UPS's remain charge it could not shutdown devices and the UPS when the remain charge was less than 40%.

I visited the NUT Hardware compatibility list page https://networkupstools.org/stable-hcl.html, selected the device type / model. The closest model I could find was "VI 2200 SH" and the driver was usbhid-ups.

I modified ups.conf and tried reloading the driver. I got am error message that the driver failed to start.

I noticed the blazer_usb driver and decided to try that.

Output:


Network UPS Tools - UPS driver controller 2.8.1
Network UPS Tools - Megatec/Q1 protocol USB driver 0.17 (2.8.1)
dstate_setflags: base variable (battery.charge.low) is immutable
Duplicate driver instance detected (PID file /run/nut/blazer_usb-pve_ups.pid exists)! Terminating other driver!
Please note that this driver is deprecated and will not receive
new development. If it works for managing your devices - fine,
but if you are running it to try setting up a new device, please
consider the newer nutdrv_qx instead, which should handle all 'Qx'
protocol variants for NUT. (Please also report if your device works
with this driver, but nutdrv_qx would not actually support it with
any subdriver!)

Supported UPS detected with megatec protocol
Vendor information unavailable
No values provided for battery high/low voltages in ups.conf

Using 'guesstimation' (low: 20.800000, high: 26.000000)!
Battery runtime will not be calculated (runtimecal not set)

Rerunning upsc pve_ups@localhost


Init SSL without certificate database
battery.charge: 100
battery.charge.low: 40
battery.voltage: 27.10
battery.voltage.high: 26.00
battery.voltage.low: 20.80
battery.voltage.nominal: 24.0
device.type: ups
driver.debug: 0
driver.flag.allow_killpower: 0
driver.name: blazer_usb
driver.parameter.default.battery.charge.low: 40
driver.parameter.override.battery.charge.low: 40
driver.parameter.pollinterval: 2
driver.parameter.port: auto
driver.parameter.productid: 5161
driver.parameter.synchronous: auto
driver.parameter.vendorid: 0665
driver.state: quiet
driver.version: 2.8.1
driver.version.internal: 0.17
driver.version.usb: libusb-1.0.28 (API: 0x100010a)
input.current.nominal: 9.0
input.frequency: 49.8
input.frequency.nominal: 50
input.voltage: 240.6
input.voltage.fault: 241.1
input.voltage.nominal: 230
output.voltage: 241.1
ups.beeper.status: enabled
ups.delay.shutdown: 30
ups.delay.start: 180
ups.load: 7
ups.productid: 5161
ups.status: OL
ups.type: offline / line interactive
ups.vendorid: 0665

While the battery.charge was showing I decided to follow the driver's advice and check whether I could get it to work with the nutdrv_qx specifying the protocol rather than leaving it on auto. Asking AI, it returned that "The q1 protocol is the foundational "Megatec" dialect. It is often the correct choice for Powerwalker units that are older or use a simplified firmware set."

While this config loaded it did not return the charge.

From https://networkupstools.org/docs/man/nutdrv_qx.html I got a list of all the protocols and decided to try each one in turn. q1 was the only protocol that worked but it did not output the charge.

I therefore decided to revert to the blazer_usb driver.

The updated version of /etc/nut/ups.conf:


# Global Directives: Define maximum start delay and polling intervals
maxretry = 3
pollinterval = 2

[pve_ups]
    driver = blazer_usb
    # driver = nutdrv_qx  # q1 protocol does not return charge informaiton
    # driver = usbhid-ups # Driver failed to start (exit status=1)
    port = auto
    vendorid = 0665
    productid = 5161
    desc = "INNO TECH USB UPS"
    # Set low battery threshold to 40%
    default.battery.charge.low = 40
    override.battery.charge.low = 40
    # Optional: override protocol if needed
    # protocol = q1
    # Comment out this line if your UPS allows you to write to you don't get a >
    override.battery.charge.low = 40

NUT Operating Mode (nut.conf)

Set NUT to standalone mode (single UPS, single system):


nano /etc/nut/nut.conf

Set:


MODE=standalone

This instructs NUT to run as a single-system setup (one UPS, one computer), starting all three layers (driver, upsd, upsmon) together.

NUT Server Configuration (upsd.conf)

Edit /etc/nut/upsd.conf to restrict listening to localhost:


nano /etc/nut/upsd.conf

Add lines:


LISTEN 127.0.0.1 3493  
# LISTEN ::1 3493   # uncomment if IPv6 is enabled

This makes the UPSD server accept connections only from the local machine.

Creating NUT Users (upsd.users)

Edit /etc/nut/upsd.users to define users for monitoring and administration:


[upsmon]  
	password = upsmon_pass   # Change to a secure password  
    upsmon master            # Grants monitoring (and master) privileges  
  
[upsadmin]  
    password = upsadmin_pass # Change to a secure password  
    actions = SET  
    instcmds = ALL
  • upsmon: monitoring user (with “master” flag for directly-connected UPS).
  • upsadmin: administrative user allowed to issue SET and instant commands.

Secure the file permissions, since it contains plain-text passwords:


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

This ensures only root or NUT group can read it.

Configuring upsmon (upsmon.conf)

The upsmon daemon monitors UPS status and initiates shutdown when needed. In /etc/nut/upsmon.conf, configure like this:


nano /etc/nut/upsmon.conf

MONITOR pve_ups@localhost 1 upsmon upsmon_pass master  
HOSTSYNC 300        # Timeout for secondaries (not used here, set to 5 min)  
MINSUPPLIES 1       # Minimum number of UPS that must be on-line  
FINALDELAY 300      # Wait 300s after shutdown before UPS cuts power  
SHUTDOWNCMD "/sbin/shutdown -h now"  
NOTIFYCMD /etc/nut/notify.sh  
  
# Notification flags: execute script and log on these events  
NOTIFYFLAG ONLINE SYSLOG+WALL  
NOTIFYFLAG ONBATT SYSLOG+WALL+EXEC  
NOTIFYFLAG LOWBATT SYSLOG+WALL+EXEC  
NOTIFYFLAG COMMOK SYSLOG+WALL  
NOTIFYFLAG COMMBAD SYSLOG+WALL+EXEC  
NOTIFYFLAG SHUTDOWN SYSLOG+WALL+EXEC  
  
POLLFREQ 5  
POLLFREQALERT 5  
DEADTIME 15         # Seconds since lost communication before COMMBAD  
POWERDOWNFLAG /etc/killpower
  • The MONITOR line lists the UPS name, host, capacity (1 = master), user, password, and “master”.
  • HOSTSYNC (300s) is used in multi-computer setups; here it can be set high to avoid premature exit.
  • MINSUPPLIES 1 means shutdown if 1 (our only) UPS is on battery and low.
  • FINALDELAY gives the OS up to 5 minutes to shut down gracefully before the UPS cuts power.
  • SHUTDOWNCMD is the command to halt the machine.
  • NOTIFYCMD points to our script for sending emails.
  • NOTIFYFLAG lines specify which events trigger notifications (e.g. ONBATT = on battery).
  • POLLFREQ/ALERT and DEADTIME control polling intervals and communication loss handling.
  • POWERDOWNFLAG (with /etc/killpower) can be used to send the UPS the “FSD” (forced shutdown) command after the OS halts.

Think of upsmon as the “safety officer” of NUT. It checks status regularly and triggers the shutdown sequence when conditions are met.

Email Notification Script

Create a script /etc/nut/notify.sh that upsmon will call on events:


nano /etc/nut/notify.sh

#!/bin/bash
# NUT UPS event notification script
# Called by upsmon with the event message as $1
 
RECIPIENT="email@addr.com"               # <-- Change this
HOSTNAME=$(hostname -f)
SUBJECT="[UPS Alert] $HOSTNAME: $1"
 
echo "UPS Event on $HOSTNAME at $(date): $1" | mail -s "$SUBJECT" "$RECIPIENT"

This script uses the mail command to send alerts. For information on how to setup Postfix click here. If you have a different smtp solution you need to adjust the script.

Make it executable and owned by root:nut:


chmod +x /etc/nut/notify.sh
chown root:nut /etc/nut/notify.sh

Test to ensure that emails transmissions are working.

Starting and Enabling Services

Now start the NUT services in order and enable them on boot:


# Start the UPS driver
systemctl start nut-driver@pve_ups
systemctl enable nut-driver@pve_ups

# Start upsd (the UPS network daemon)
systemctl start nut-server
systemctl enable nut-server

# Start upsmon (the monitoring daemon)
systemctl start nut-monitor
systemctl enable nut-monitor

Check their status:


systemctl status nut-driver@pve_ups nut-server nut-monitor

Expected output (active/running) should indicate all services are up. For example:


● nut-driver@pve_ups.service - Network UPS Tools - device driver for NUT device 'pve_ups'
     Loaded: loaded (/usr/lib/systemd/system/nut-driver@.service; enabled; preset: enabled)
    Drop-In: /etc/systemd/system/nut-driver@pve_ups.service.d
             └─nut-driver-enumerator-generated-checksum.conf, nut-driver-enumerator-generated.conf
     Active: active (running) since Thu 2026-03-05 16:39:06 CET; 27min ago
 Invocation: e6871899709b42759300b9b8f9b913a6
    Process: 670956 ExecStartPre=/usr/bin/systemd-tmpfiles --create /usr/lib/tmpfiles.d/nut-common-tmpfiles.conf (code=exited, status=0/SUCCESS)
    Process: 670958 ExecStart=/bin/sh -c NUTDEV="`/usr/libexec/nut-driver-enumerator.sh --get-device-for-service pve_ups`" && [ -n "$NUTDEV" ] || { echo "FATAL: Could not find a NUT device section for service unit pve_ups" >&2 ; exit 1 >
   Main PID: 671052 (blazer_usb)
      Tasks: 1 (limit: 154028)
     Memory: 976K (peak: 5.2M)
        CPU: 295ms
     CGroup: /system.slice/system-nut\x2ddriver.slice/nut-driver@pve_ups.service
             └─671052 /usr/lib/nut/blazer_usb -a pve_ups

Mar 05 16:39:05 pve blazer_usb[670985]: No values provided for battery high/low voltages in ups.conf
Mar 05 16:39:05 pve blazer_usb[670985]: Using 'guesstimation' (low: 20.800000, high: 26.000000)!
Mar 05 16:39:05 pve blazer_usb[670985]: Battery runtime will not be calculated (runtimecal not set)
Mar 05 16:39:06 pve blazer_usb[671052]: Startup successful
Mar 05 16:39:06 pve nut-driver@pve_ups[670958]: Network UPS Tools - UPS driver controller 2.8.1
Mar 05 16:39:06 pve blazer_usb[671052]: upsnotify: notify about state 2 with libsystemd: was requested, but not running as a service unit now, will not spam more about it
Mar 05 16:39:06 pve systemd[1]: Started nut-driver@pve_ups.service - Network UPS Tools - device driver for NUT device 'pve_ups'.
Mar 05 16:39:06 pve blazer_usb[671052]: upsnotify: failed to notify about state 2: no notification tech defined, will not spam more about it
Mar 05 16:39:06 pve blazer_usb[671052]: upsnotify: logged the systemd watchdog situation once, will not spam more about it
Mar 05 16:39:07 pve blazer_usb[671052]: sock_connect: enabling asynchronous mode (auto)

● nut-server.service - Network UPS Tools - power devices information server
     Loaded: loaded (/usr/lib/systemd/system/nut-server.service; enabled; preset: enabled)
     Active: active (running) since Wed 2026-03-04 14:10:17 CET; 1 day 2h ago
 Invocation: 1b3b4b947bd9434fb671065fc9a6ccec
    Process: 670778 ExecReload=/usr/sbin/upsd -c reload -P $MAINPID (code=exited, status=0/SUCCESS)
   Main PID: 61357 (upsd)
      Tasks: 1 (limit: 154028)
     Memory: 1M (peak: 3.2M)
        CPU: 4.029s
     CGroup: /system.slice/nut-server.service
             └─61357 /lib/nut/upsd -F

Mar 05 16:38:43 pve nut-server[61357]: mainloop: Interrupted system call
Mar 05 16:38:43 pve nut-server[61357]: SIGHUP: reloading configuration
Mar 05 16:38:43 pve nut-server[61357]: Applying debug level 0 from original command line arguments
Mar 05 16:38:43 pve upsd[61357]: SIGHUP: reloading configuration
Mar 05 16:38:43 pve upsd[61357]: Applying debug level 0 from original command line arguments
Mar 05 16:38:43 pve systemd[1]: Reloaded nut-server.service - Network UPS Tools - power devices information server.
Mar 05 16:38:55 pve nut-server[61357]: Connected to UPS [pve_ups]: blazer_usb-pve_ups
Mar 05 16:38:55 pve upsd[61357]: Connected to UPS [pve_ups]: blazer_usb-pve_ups
Mar 05 16:39:07 pve nut-server[61357]: Connected to UPS [pve_ups]: blazer_usb-pve_ups
Mar 05 16:39:07 pve upsd[61357]: Connected to UPS [pve_ups]: blazer_usb-pve_ups

● nut-monitor.service - Network UPS Tools - power device monitor and shutdown controller
     Loaded: loaded (/usr/lib/systemd/system/nut-monitor.service; enabled; preset: enabled)
     Active: active (running) since Wed 2026-03-04 15:53:25 CET; 1 day 1h ago
 Invocation: e34c6442628343bfb3500fc8a01c2fea
   Main PID: 107450 (upsmon)
      Tasks: 2 (limit: 154028)
     Memory: 1.1M (peak: 6M)
        CPU: 1.828s
     CGroup: /system.slice/nut-monitor.service
             ├─107450 /lib/nut/upsmon -F
             └─107451 /lib/nut/upsmon -F

Mar 05 16:38:43 pve nut-monitor[107451]: Poll UPS [pve_ups@localhost] failed - Driver not connected
Mar 05 16:38:48 pve nut-monitor[107451]: Poll UPS [pve_ups@localhost] failed - Driver not connected
Mar 05 16:38:53 pve nut-monitor[107451]: Poll UPS [pve_ups@localhost] failed - Driver not connected
Mar 05 16:38:58 pve nut-monitor[107451]: Communications with UPS pve_ups@localhost established
Mar 05 16:38:58 pve nut-monitor[670944]: Network UPS Tools upsmon 2.8.1
Mar 05 16:39:03 pve nut-monitor[107451]: Poll UPS [pve_ups@localhost] failed - Driver not connected
Mar 05 16:39:03 pve nut-monitor[107451]: Communications with UPS pve_ups@localhost lost
Mar 05 16:39:03 pve nut-monitor[671005]: Network UPS Tools upsmon 2.8.1
Mar 05 16:39:08 pve nut-monitor[107451]: Communications with UPS pve_ups@localhost established
Mar 05 16:39:08 pve nut-monitor[671063]: Network UPS Tools upsmon 2.8.1

This confirms the driver (blazer_usb), upsd, and upsmon daemons are running and connected to the UPS.

USB Permissions (udev Rule)

To ensure NUT can always access the UPS USB device after reboots, add a udev rule:


nano /etc/udev/rules.d/99-nut-ups.rules

ATTR{idVendor}<mark style="background-color: #fff3cd; padding: 2px 4px;">"0665", ATTR{idProduct}</mark>"5161", MODE="0660", GROUP="nut"

Use your vendor/product IDs. Then reload udev rules:


udevadm control --reload-rules
udevadm trigger

This makes the UPS device always belong to group nut with correct permissions, so nut -group processes can access it.

Testing Shutdown Logic

Perform controlled tests (save work before doing this):

Forced Shutdown Command

Send an FSD (forced shutdown) flag to upsmon:


# Send the FSD (Forced Shutdown) command
upsmon -c fsd

This should start the shutdown sequence immediately.

Unplug UPS

Disconnect mains power from the UPS. Then run:


watch -n 2 upsc pve_ups@localhost

You should see ups.status change from OL (on line) to OB (on battery). and the battery.charge start to go down.

Also monitor logs:


journalctl -f -g 'ups|nut' --case-sensitive=false --no-pager

Verify that upsmon logs a low-battery or on-battery event and triggers shutdown after the configured delay.

Low Battery Simulation

If your UPS firmware allows changing the low threshold, you can trigger a LOWBATT event manually. In one terminal:


journalctl -f -g 'ups|nut' --case-sensitive=false --no-pager

In another:


upsrw -s battery.charge.low=99 -u upsadmin -p upsadmin_pass pve_ups@localhost

This artificially raises the low threshold above the current charge, causing upsmon to see a LOWBATT event. (If the charge is at 100 just unplug the UPS for a few seconds. The first terminal’s logs should show the notification.

Then plug in the UPS and restore the settings


upsrw -s battery.charge.low=40 -u upsadmin -p upsadmin_pass pve_ups@localhost

 


 

Follow This, That and (Maybe), the Other:

<



Comments

Popular posts from this blog

How to clone and synchronise a GitHub repository on Android

The complete guide to installing, configuring, and managing Plex Media Server on an Ubuntu Server