APC Back-UPS BX950MI on Raspberry Pi 5
This document describes the working setup for monitoring an APC Back-UPS BX950MI connected to a Raspberry Pi 5 over USB.
The setup uses:
- NUT (Network UPS Tools) for communication with the UPS
usbhid-upsas the USB/HID driverupsdas the NUT serverupscto read UPS values- RRDTool for historical data
- A Python script to periodically collect the UPS data and generate graphs
The UPS is connected to the Raspberry Pi using its USB data cable.
1. Hardware
UPS:
American Power Conversion
Back-UPS BX950MI
USB identification:
Vendor ID: 051d
Product ID: 0002
Serial: 9B2545A39513
The Raspberry Pi detects the UPS as:
American Power Conversion Uninterruptible Power Supply
The USB device can be verified with:
lsusb
Expected output includes:
Bus 002 Device 004: ID 051d:0002 American Power Conversion Uninterruptible Power Supply
More detailed information:
lsusb -v -d 051d:0002
The device identifies itself as:
Manufacturer: American Power Conversion
Product: Back-UPS BX950MI
2. Install NUT
Install the NUT packages:
sudo apt install nut nut-client nut-server
Also install RRDTool for the historical data collector:
sudo apt install librrd-dev python3-dev python3-venv
cd /opt/ups
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
The collector uses the virtual environment at /opt/ups/.venv. Calling its
Python executable directly means cron does not need to activate the environment.
Check the NUT version:
nut-scanner --version
The working installation used:
Network UPS Tools 2.8.1
3. USB detection
The Raspberry Pi kernel recognizes the UPS as a USB HID device.
The relevant kernel message is:
hid-generic 0003:051D:0002.0003:
hiddev0,hidraw0:
USB HID v1.10 Device
[American Power Conversion Back-UPS BX950MI]
The device is therefore using the standard USB HID interface.
The relevant kernel modules are:
lsmod | grep hid
which showed:
hid_generic
usbhid
The USB interface is:
Interface Class: HID
Endpoint: EP 1 IN
Transfer Type: Interrupt
This is exactly what the NUT usbhid-ups driver is designed to communicate with.
4. Finding the UPS with nut-scanner
The USB scanner was eventually able to identify the UPS with:
sudo nut-scanner -U
The important part of the result was:
[nutdev1]
driver = "usbhid-ups"
port = "auto"
vendorid = "051D"
productid = "0002"
product = "Back-UPS BX950MI"
serial = "9B2545A39513"
vendor = "American Power Conversion"
The important information is therefore:
driver = usbhid-ups
vendorid = 051D
productid = 0002
serial = 9B2545A39513
The other warnings from nut-scanner about missing optional libraries are not relevant to this USB UPS.
For example:
Cannot load SNMP library
Cannot load XML library
Cannot load AVAHI library
Cannot load IPMI library
Those simply mean that NUT cannot scan those other types of UPS/network devices.
5. NUT configuration
The UPS is configured in:
/etc/nut/ups.conf
The working configuration is:
[BX950MI]
driver = usbhid-ups
port = auto
vendorid = 051D
productid = 0002
serial = 9B2545A39513
The name:
BX950MI
is important.
It becomes the UPS name used by commands such as:
upsc BX950MI
6. NUT operating mode
The NUT operating mode is configured in:
/etc/nut/nut.conf
The required setting is:
MODE=standalone
This tells NUT that the Raspberry Pi is operating as a standalone UPS monitoring system.
It allows the local NUT server (upsd) and monitor to operate on the Raspberry Pi.
7. NUT users
NUT authentication is configured in:
/etc/nut/upsd.users
The working monitoring user is:
[monuser]
password = elvis
upsmon primary
This user is used by upsmon to monitor the UPS.
The password should of course be changed if this machine is accessible to other users.
A stronger example would be:
[monuser]
password = <strong-password>
upsmon primary
8. NUT server
The NUT server is upsd.
Its job is to provide UPS information to NUT clients such as:
upsc
upsmon
The server listens on:
127.0.0.1:3493
The successful startup showed:
listening on 127.0.0.1 port 3493
and:
Connected to UPS [BX950MI]: usbhid-ups-BX950MI
This confirms that:
upsdis running- the UPS is defined
- the
usbhid-upsdriver is running upsdcan communicate with the driver
Check it with:
sudo systemctl status nut-server
A successful status contains:
Active: active (running)
and:
Connected to UPS [BX950MI]
9. NUT driver
The actual USB communication is handled by:
usbhid-ups
On this system it runs as the systemd service:
nut-driver@BX950MI.service
Check it with:
sudo systemctl status nut-driver@BX950MI
The successful configuration showed:
Active: active (running)
and:
Using subdriver: APC HID 0.100
followed by:
Startup successful
This is the important indication that the UPS is correctly communicating with NUT.
10. Reading UPS information
The easiest way to test the complete setup is:
upsc BX950MI
The working UPS returned:
battery.charge: 100
battery.charge.low: 10
battery.mfr.date: 2001/01/01
battery.runtime: 3584
battery.runtime.low: 120
battery.type: PbAc
battery.voltage: 13.6
battery.voltage.nominal: 12.0
device.mfr: American Power Conversion
device.model: Back-UPS BX950MI
device.serial: 9B2545A39513
device.type: ups
input.sensitivity: medium
input.transfer.high: 295
input.transfer.low: 145
input.voltage: 234.0
input.voltage.nominal: 230
ups.beeper.status: enabled
ups.load: 0
ups.mfr: American Power Conversion
ups.mfr.date: 2025/11/16
ups.model: Back-UPS BX950MI
ups.productid: 0002
ups.realpower.nominal: 520
ups.serial: 9B2545A39513
ups.status: OL
ups.test.result: Done and passed
ups.vendorid: 051d
This confirms that the complete chain is working:
APC UPS
│
│ USB
▼
Raspberry Pi USB
│
▼
Linux HID
│
▼
usbhid-ups
│
▼
NUT
│
▼
upsc
11. Important UPS values
The Python collector uses these NUT values:
Battery charge
battery.charge
Current example:
100 %
This is the UPS's estimated battery charge.
Battery runtime
battery.runtime
NUT reports this in seconds.
Example:
3584
which is approximately:
59.7 minutes
The Python graphs convert this to minutes.
UPS load
ups.load
Example:
0 %
This is the estimated current load on the UPS.
Nominal power
ups.realpower.nominal
Example:
520 W
This is the nominal real-power capacity reported by the UPS.
It should not be confused with actual current power consumption.
UPS status
ups.status
Example:
OL
The commonly encountered values are:
OL = On Line
OB = On Battery
LB = Low Battery
RB = Replace Battery
The Python collector converts these into numeric values for RRDTool.
12. The Python RRD collector
The Python program is:
/opt/ups/ups2rrd.py
Its job is deliberately simple:
NUT
│
│ PyNUTClient
▼
Python
│
├── battery.charge
├── battery.runtime
├── ups.load
├── ups.realpower.nominal
└── ups.status
│
▼
RRDTool
│
▼
/var/lib/rrd/bx950mi.rrd
│
▼
PNG graphs
The script performs four main tasks:
- Create the RRD database if it doesn't exist
- Read the current UPS values
- Update the RRD and regenerate the graphs
- Generate the HTML dashboard
13. RRD database
The database is:
/var/lib/rrd/bx950mi.rrd
RRDTool means:
Round Robin Database
It is particularly suitable for time-series measurements such as UPS monitoring.
Instead of growing indefinitely, the database has predefined storage periods.
Old high-resolution data is automatically represented at lower resolution.
14. One-minute collection
The RRD uses a primary step of:
60 seconds
The corresponding configuration is:
"--step",
"60",
The cron job runs the Python script every minute:
* * * * * /opt/ups/.venv/bin/python /opt/ups/ups2rrd.py >> /var/log/ups_rrd.log 2>&1
Therefore, approximately one measurement is stored every minute.
15. RRD retention
The database uses different resolutions for different periods.
Three months
The status and load retain:
1 minute
for approximately:
90 days
This is useful for detecting short power failures.
For example, an event like:
18:21 OL
18:22 OL
18:23 OB
18:24 OB
18:25 OB
18:26 OL
will clearly show up as a battery event.
One year
Battery charge, battery runtime and nominal power are retained at:
5 minute
resolution.
This is enough detail to see long-term battery behaviour without requiring every minute for the entire year.
Five years
Long-term data is consolidated to:
1 hour
resolution.
This keeps the database small while allowing trends such as battery degradation to be observed over several years.
16. Status conversion
RRDTool works with numeric values, so the Python program converts the UPS status.
The mapping is:
STATUS_VALUES = {
"OL": 0,
"OB": 1,
"LB": 2,
"RB": 3,
"UNKNOWN": 4,
}
Therefore:
0 = On Line
1 = On Battery
2 = Low Battery
3 = Replace Battery
4 = Unknown
The graph can then display the status history.
17. Handling missing data
The Python program converts unavailable measurements into:
U
which means:
Unknown
in RRDTool.
For example, if NUT temporarily fails to return:
battery.runtime
the script doesn't write a bogus zero.
Instead it writes:
U
RRDTool will leave a gap in the graph.
This is important because:
0 seconds
and:
measurement unavailable
are completely different things.
18. Generated graphs
The script creates:
/var/www/html/ups/index.html
/var/www/html/ups/week.html
/var/www/html/ups/status-3m.png
/var/www/html/ups/status-1w.png
/var/www/html/ups/battery-1w.png
/var/www/html/ups/battery-1y.png
/var/www/html/ups/battery-5y.png
/var/www/html/ups/runtime-1w.png
/var/www/html/ups/runtime-1y.png
/var/www/html/ups/runtime-5y.png
/var/www/html/ups/load-1w.png
/var/www/html/ups/load-3m.png
/var/www/html/ups/power-1w.png
/var/www/html/ups/power-1y.png
/var/www/html/ups/power-5y.png
If /var/www/html is served by the web server, these can therefore be viewed as:
/ups/
/ups/week.html
/ups/status-3m.png
/ups/status-1w.png
/ups/battery-1w.png
/ups/battery-1y.png
/ups/battery-5y.png
/ups/runtime-1w.png
/ups/runtime-1y.png
/ups/runtime-5y.png
/ups/load-1w.png
/ups/load-3m.png
/ups/power-1w.png
/ups/power-1y.png
/ups/power-5y.png
The main responsive dashboard groups the long-term graphs on one page. The
weekly dashboard at /ups/week.html shows one-week history for every collected
value. The pages link to each other, refresh every 60 seconds, and open a
full-size PNG when a graph is clicked.
19. Testing the Python program
Run it manually:
sudo /opt/ups/.venv/bin/python /opt/ups/ups2rrd.py
A successful run should produce something similar to:
2026-08-10 18:00:00 status=OL battery=100.0 runtime=3584.0 load=0.0 nominal=520.0
Check the database:
ls -lh /var/lib/rrd/bx950mi.rrd
Check the most recent RRD values:
rrdtool lastupdate /var/lib/rrd/bx950mi.rrd
20. Checking cron
Install the cron entry in root's crontab because the collector writes to
/var/lib/rrd and /var/www/html/ups:
sudo crontab -e
Add:
* * * * * /opt/ups/.venv/bin/python /opt/ups/ups2rrd.py >> /var/log/ups_rrd.log 2>&1
There is no need to run source, activate the virtual environment, or use a
wrapper script.
After a few minutes:
tail -f /var/log/ups_rrd.log
You should see a new line approximately every minute.
21. Checking NUT services
Useful commands:
sudo systemctl status nut-driver@BX950MI
sudo systemctl status nut-server
And:
sudo systemctl list-units --all 'nut*'
The important services are:
nut-driver@BX950MI.service
nut-server.service
The driver should be:
active (running)
The server should also be:
active (running)
22. Troubleshooting
UPS not detected
Check:
lsusb
The UPS should appear as:
051d:0002
Then:
sudo nut-scanner -U
Driver not running
Check:
sudo systemctl status nut-driver@BX950MI
Then:
sudo journalctl -u nut-driver@BX950MI -n 50
A successful driver startup contains:
Using subdriver: APC HID 0.100
and:
Startup successful
upsc says connection refused
Check:
sudo systemctl status nut-server
If nut-server isn't running, start it:
sudo systemctl start nut-server
Then:
upsc BX950MI
The server should report:
listening on 127.0.0.1 port 3493
23. USB device permissions
The UPS is a USB HID device.
NUT's usbhid-ups driver runs as the:
nut
user.
The NUT systemd service therefore needs to be allowed to access the USB device.
The working setup uses the NUT/systemd configuration supplied by the Ubuntu NUT package.
It is important not to manually run the driver as root as part of the normal operation.
For example, this is useful for debugging:
sudo /lib/nut/usbhid-ups -a BX950MI -DD
but normal operation should be handled by:
nut-driver@BX950MI.service
24. Current working architecture
The complete system now looks like this:
APC Back-UPS BX950MI
│
│ USB
▼
Raspberry Pi 5
│
▼
Linux USB HID
│
▼
usbhid-ups
│
▼
NUT
┌─────────────┴─────────────┐
│ │
upsd upsmon
│
│ PyNUTClient
▼
Python script
│
▼
RRDTool
│
▼
bx950mi.rrd
│
▼
PNG graphs
The important point is that Python does not communicate directly with the UPS.
It communicates with the NUT server through a Python client:
from PyNUTClient import PyNUT
client = PyNUT.PyNUTClient(host="127.0.0.1")
values = client.GetUPSVars("BX950MI")
This is preferable because NUT handles all of the USB/HID details.
The Python program only needs to understand normal NUT values.
25. Useful commands
Show all UPS values:
upsc BX950MI
Show battery charge:
upsc BX950MI battery.charge
Show battery runtime:
upsc BX950MI battery.runtime
Show UPS load:
upsc BX950MI ups.load
Show UPS status:
upsc BX950MI ups.status
Show nominal power:
upsc BX950MI ups.realpower.nominal
Check USB:
lsusb
Check NUT driver:
sudo systemctl status nut-driver@BX950MI
Check NUT server:
sudo systemctl status nut-server
Check recent driver messages:
sudo journalctl -u nut-driver@BX950MI -n 50
Check recent server messages:
sudo journalctl -u nut-server -n 50
Check RRD:
rrdtool info /var/lib/rrd/bx950mi.rrd
Check latest RRD values:
rrdtool lastupdate /var/lib/rrd/bx950mi.rrd
Check Python collector:
tail -f /var/log/ups_rrd.log
26. Summary
The successful setup consists of four layers:
1. USB
The Raspberry Pi detects:
051d:0002
American Power Conversion Back-UPS BX950MI
2. NUT driver
usbhid-ups
communicates with the UPS using USB HID.
3. NUT server
upsd
provides the UPS information locally on:
127.0.0.1:3493
4. Python + RRDTool
The Python program runs every minute, obtains selected values from:
upsd on 127.0.0.1:3493 (through PyNUTClient)
and stores them in:
/var/lib/rrd/bx950mi.rrd
RRDTool then provides historical data for:
3 months @ 1 minute
1 year @ 5 minutes
5 years @ 1 hour
This provides a lightweight UPS monitoring system running entirely on the Raspberry Pi, without requiring an external monitoring service.