# 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-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 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: ```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 = 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 /opt/ups/ups2rrd.py ``` 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 ``` The script performs four main tasks: 1. Create the RRD database if it doesn't exist 2. Read the current UPS values 3. Update the RRD and regenerate the graphs 4. Generate the HTML dashboard --- # 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 * * * * * /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: ```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 /var/www/html/ups/index.html /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 /ups/ /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 ``` The responsive dashboard groups all graphs on one page and refreshes every 60 seconds. Clicking a graph opens its full-size PNG. --- # 19. Testing the Python program Run it manually: ```bash sudo /opt/ups/.venv/bin/python /opt/ups/ups2rrd.py ``` 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 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: ```cron * * * * * /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: ```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 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: ```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. ```