2026-08-10 18:51:41 +02:00
|
|
|
# APC Back-UPS BX950MI on Raspberry Pi 5
|
2026-08-10 18:31:14 +02:00
|
|
|
|
2026-08-10 18:51:41 +02:00
|
|
|
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-ups` as the USB/HID driver
|
|
|
|
|
- `upsd` as the NUT server
|
|
|
|
|
- `upsc` to 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:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
American Power Conversion
|
|
|
|
|
Back-UPS BX950MI
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
USB identification:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
Vendor ID: 051d
|
|
|
|
|
Product ID: 0002
|
|
|
|
|
Serial: 9B2545A39513
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The Raspberry Pi detects the UPS as:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
American Power Conversion Uninterruptible Power Supply
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The USB device can be verified with:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
lsusb
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Expected output includes:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
Bus 002 Device 004: ID 051d:0002 American Power Conversion Uninterruptible Power Supply
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
More detailed information:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
lsusb -v -d 051d:0002
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The device identifies itself as:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
Manufacturer: American Power Conversion
|
|
|
|
|
Product: Back-UPS BX950MI
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# 2. Install NUT
|
|
|
|
|
|
|
|
|
|
Install the NUT packages:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo apt install nut nut-client nut-server
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Also install RRDTool for the historical data collector:
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-08-10 19:07:26 +02:00
|
|
|
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
|
2026-08-10 18:51:41 +02:00
|
|
|
```
|
|
|
|
|
|
2026-08-10 19:07:26 +02:00
|
|
|
The collector uses the virtual environment at `/opt/ups/.venv`. Calling its
|
|
|
|
|
Python executable directly means cron does not need to activate the environment.
|
|
|
|
|
|
2026-08-10 18:51:41 +02:00
|
|
|
Check the NUT version:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
nut-scanner --version
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The working installation used:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
lsmod | grep hid
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
which showed:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
hid_generic
|
|
|
|
|
usbhid
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The USB interface is:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo nut-scanner -U
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The important part of the result was:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
[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:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
/etc/nut/ups.conf
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The working configuration is:
|
|
|
|
|
|
|
|
|
|
```ini
|
|
|
|
|
[BX950MI]
|
|
|
|
|
driver = usbhid-ups
|
|
|
|
|
port = auto
|
|
|
|
|
vendorid = 051D
|
|
|
|
|
productid = 0002
|
|
|
|
|
serial = 9B2545A39513
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The name:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
BX950MI
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
is important.
|
|
|
|
|
|
|
|
|
|
It becomes the UPS name used by commands such as:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
upsc BX950MI
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# 6. NUT operating mode
|
|
|
|
|
|
|
|
|
|
The NUT operating mode is configured in:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
/etc/nut/nut.conf
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The required setting is:
|
|
|
|
|
|
|
|
|
|
```ini
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
/etc/nut/upsd.users
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The working monitoring user is:
|
|
|
|
|
|
|
|
|
|
```ini
|
|
|
|
|
[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:
|
|
|
|
|
|
|
|
|
|
```ini
|
|
|
|
|
[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:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
upsc
|
|
|
|
|
upsmon
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The server listens on:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
127.0.0.1:3493
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The successful startup showed:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
listening on 127.0.0.1 port 3493
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
and:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
Connected to UPS [BX950MI]: usbhid-ups-BX950MI
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
This confirms that:
|
|
|
|
|
|
|
|
|
|
1. `upsd` is running
|
|
|
|
|
2. the UPS is defined
|
|
|
|
|
3. the `usbhid-ups` driver is running
|
|
|
|
|
4. `upsd` can communicate with the driver
|
|
|
|
|
|
|
|
|
|
Check it with:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo systemctl status nut-server
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
A successful status contains:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
Active: active (running)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
and:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
Connected to UPS [BX950MI]
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# 9. NUT driver
|
|
|
|
|
|
|
|
|
|
The actual USB communication is handled by:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
usbhid-ups
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
On this system it runs as the systemd service:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
nut-driver@BX950MI.service
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Check it with:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo systemctl status nut-driver@BX950MI
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The successful configuration showed:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
Active: active (running)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
and:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
Using subdriver: APC HID 0.100
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
followed by:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
upsc BX950MI
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The working UPS returned:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
battery.charge
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Current example:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
100 %
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
This is the UPS's estimated battery charge.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Battery runtime
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
battery.runtime
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
NUT reports this in seconds.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
3584
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
which is approximately:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
59.7 minutes
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The Python graphs convert this to minutes.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## UPS load
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
ups.load
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
0 %
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
This is the estimated current load on the UPS.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Nominal power
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
ups.realpower.nominal
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
ups.status
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
OL
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The commonly encountered values are:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```text
|
2026-08-10 19:07:26 +02:00
|
|
|
/opt/ups/ups2rrd.py
|
2026-08-10 18:51:41 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Its job is deliberately simple:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
NUT
|
|
|
|
|
│
|
|
|
|
|
│ PyNUTClient
|
|
|
|
|
▼
|
|
|
|
|
Python
|
|
|
|
|
│
|
|
|
|
|
├── battery.charge
|
|
|
|
|
├── battery.runtime
|
|
|
|
|
├── ups.load
|
|
|
|
|
├── ups.realpower.nominal
|
|
|
|
|
└── ups.status
|
|
|
|
|
│
|
|
|
|
|
▼
|
|
|
|
|
RRDTool
|
|
|
|
|
│
|
|
|
|
|
▼
|
|
|
|
|
/var/lib/rrd/bx950mi.rrd
|
|
|
|
|
│
|
|
|
|
|
▼
|
|
|
|
|
PNG graphs
|
|
|
|
|
```
|
|
|
|
|
|
2026-08-10 19:07:26 +02:00
|
|
|
The script performs four main tasks:
|
2026-08-10 18:51:41 +02:00
|
|
|
|
|
|
|
|
1. Create the RRD database if it doesn't exist
|
|
|
|
|
2. Read the current UPS values
|
|
|
|
|
3. Update the RRD and regenerate the graphs
|
2026-08-10 19:07:26 +02:00
|
|
|
4. Generate the HTML dashboard
|
2026-08-10 18:51:41 +02:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# 13. RRD database
|
|
|
|
|
|
|
|
|
|
The database is:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
/var/lib/rrd/bx950mi.rrd
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
RRDTool means:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
60 seconds
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The corresponding configuration is:
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
"--step",
|
|
|
|
|
"60",
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The cron job runs the Python script every minute:
|
|
|
|
|
|
|
|
|
|
```cron
|
2026-08-10 19:07:26 +02:00
|
|
|
* * * * * /opt/ups/.venv/bin/python /opt/ups/ups2rrd.py >> /var/log/ups_rrd.log 2>&1
|
2026-08-10 18:51:41 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
1 minute
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
for approximately:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
90 days
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
This is useful for detecting short power failures.
|
|
|
|
|
|
|
|
|
|
For example, an event like:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
STATUS_VALUES = {
|
|
|
|
|
"OL": 0,
|
|
|
|
|
"OB": 1,
|
|
|
|
|
"LB": 2,
|
|
|
|
|
"RB": 3,
|
|
|
|
|
"UNKNOWN": 4,
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Therefore:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
U
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
which means:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
Unknown
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
in RRDTool.
|
|
|
|
|
|
|
|
|
|
For example, if NUT temporarily fails to return:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
battery.runtime
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
the script doesn't write a bogus zero.
|
|
|
|
|
|
|
|
|
|
Instead it writes:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
U
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
RRDTool will leave a gap in the graph.
|
|
|
|
|
|
|
|
|
|
This is important because:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
0 seconds
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
and:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
measurement unavailable
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
are completely different things.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# 18. Generated graphs
|
|
|
|
|
|
|
|
|
|
The script creates:
|
|
|
|
|
|
|
|
|
|
```text
|
2026-08-10 19:07:26 +02:00
|
|
|
/var/www/html/ups/index.html
|
|
|
|
|
|
2026-08-10 18:51:41 +02:00
|
|
|
/var/www/html/ups/status-3m.png
|
|
|
|
|
|
|
|
|
|
/var/www/html/ups/battery-1y.png
|
|
|
|
|
/var/www/html/ups/battery-5y.png
|
|
|
|
|
|
|
|
|
|
/var/www/html/ups/runtime-1y.png
|
|
|
|
|
/var/www/html/ups/runtime-5y.png
|
|
|
|
|
|
|
|
|
|
/var/www/html/ups/load-3m.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:
|
|
|
|
|
|
|
|
|
|
```text
|
2026-08-10 19:07:26 +02:00
|
|
|
/ups/
|
|
|
|
|
|
2026-08-10 18:51:41 +02:00
|
|
|
/ups/status-3m.png
|
|
|
|
|
/ups/battery-1y.png
|
|
|
|
|
/ups/battery-5y.png
|
|
|
|
|
/ups/runtime-1y.png
|
|
|
|
|
/ups/runtime-5y.png
|
|
|
|
|
/ups/load-3m.png
|
|
|
|
|
/ups/power-1y.png
|
|
|
|
|
/ups/power-5y.png
|
|
|
|
|
```
|
|
|
|
|
|
2026-08-10 19:07:26 +02:00
|
|
|
The responsive dashboard groups all graphs on one page and refreshes every
|
|
|
|
|
60 seconds. Clicking a graph opens its full-size PNG.
|
|
|
|
|
|
2026-08-10 18:51:41 +02:00
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# 19. Testing the Python program
|
|
|
|
|
|
|
|
|
|
Run it manually:
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-08-10 19:07:26 +02:00
|
|
|
sudo /opt/ups/.venv/bin/python /opt/ups/ups2rrd.py
|
2026-08-10 18:51:41 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
A successful run should produce something similar to:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
2026-08-10 18:00:00 status=OL battery=100.0 runtime=3584.0 load=0.0 nominal=520.0
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Check the database:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
ls -lh /var/lib/rrd/bx950mi.rrd
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Check the most recent RRD values:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
rrdtool lastupdate /var/lib/rrd/bx950mi.rrd
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# 20. Checking cron
|
|
|
|
|
|
2026-08-10 19:07:26 +02:00
|
|
|
Install the cron entry in root's crontab because the collector writes to
|
|
|
|
|
`/var/lib/rrd` and `/var/www/html/ups`:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo crontab -e
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Add:
|
2026-08-10 18:51:41 +02:00
|
|
|
|
|
|
|
|
```cron
|
2026-08-10 19:07:26 +02:00
|
|
|
* * * * * /opt/ups/.venv/bin/python /opt/ups/ups2rrd.py >> /var/log/ups_rrd.log 2>&1
|
2026-08-10 18:51:41 +02:00
|
|
|
```
|
|
|
|
|
|
2026-08-10 19:07:26 +02:00
|
|
|
There is no need to run `source`, activate the virtual environment, or use a
|
|
|
|
|
wrapper script.
|
|
|
|
|
|
2026-08-10 18:51:41 +02:00
|
|
|
After a few minutes:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
tail -f /var/log/ups_rrd.log
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
You should see a new line approximately every minute.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# 21. Checking NUT services
|
|
|
|
|
|
|
|
|
|
Useful commands:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo systemctl status nut-driver@BX950MI
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo systemctl status nut-server
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
And:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo systemctl list-units --all 'nut*'
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The important services are:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
nut-driver@BX950MI.service
|
|
|
|
|
nut-server.service
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The driver should be:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
active (running)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The server should also be:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
active (running)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# 22. Troubleshooting
|
|
|
|
|
|
|
|
|
|
## UPS not detected
|
|
|
|
|
|
|
|
|
|
Check:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
lsusb
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The UPS should appear as:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
051d:0002
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Then:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo nut-scanner -U
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Driver not running
|
|
|
|
|
|
|
|
|
|
Check:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo systemctl status nut-driver@BX950MI
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Then:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo journalctl -u nut-driver@BX950MI -n 50
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
A successful driver startup contains:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
Using subdriver: APC HID 0.100
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
and:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
Startup successful
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## `upsc` says connection refused
|
|
|
|
|
|
|
|
|
|
Check:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo systemctl status nut-server
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
If `nut-server` isn't running, start it:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo systemctl start nut-server
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Then:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
upsc BX950MI
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The server should report:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo /lib/nut/usbhid-ups -a BX950MI -DD
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
but normal operation should be handled by:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
nut-driver@BX950MI.service
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# 24. Current working architecture
|
|
|
|
|
|
|
|
|
|
The complete system now looks like this:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```python
|
2026-08-10 18:58:29 +02:00
|
|
|
from PyNUTClient import PyNUT
|
|
|
|
|
|
2026-08-10 18:51:41 +02:00
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
upsc BX950MI
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Show battery charge:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
upsc BX950MI battery.charge
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Show battery runtime:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
upsc BX950MI battery.runtime
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Show UPS load:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
upsc BX950MI ups.load
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Show UPS status:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
upsc BX950MI ups.status
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Show nominal power:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
upsc BX950MI ups.realpower.nominal
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Check USB:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
lsusb
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Check NUT driver:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo systemctl status nut-driver@BX950MI
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Check NUT server:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo systemctl status nut-server
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Check recent driver messages:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo journalctl -u nut-driver@BX950MI -n 50
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Check recent server messages:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo journalctl -u nut-server -n 50
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Check RRD:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
rrdtool info /var/lib/rrd/bx950mi.rrd
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Check latest RRD values:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
rrdtool lastupdate /var/lib/rrd/bx950mi.rrd
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Check Python collector:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
tail -f /var/log/ups_rrd.log
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# 26. Summary
|
|
|
|
|
|
|
|
|
|
The successful setup consists of four layers:
|
|
|
|
|
|
|
|
|
|
### 1. USB
|
|
|
|
|
|
|
|
|
|
The Raspberry Pi detects:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
051d:0002
|
|
|
|
|
American Power Conversion Back-UPS BX950MI
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 2. NUT driver
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
usbhid-ups
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
communicates with the UPS using USB HID.
|
|
|
|
|
|
|
|
|
|
### 3. NUT server
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
upsd
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
provides the UPS information locally on:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
127.0.0.1:3493
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 4. Python + RRDTool
|
|
|
|
|
|
|
|
|
|
The Python program runs every minute, obtains selected values from:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
upsd on 127.0.0.1:3493 (through PyNUTClient)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
and stores them in:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
/var/lib/rrd/bx950mi.rrd
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
RRDTool then provides historical data for:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
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.
|
|
|
|
|
```
|