APC Back‐UPS BX‐series: fixing spurious LOWBATT REPLBATT events - networkupstools/nut GitHub Wiki
This page documents a working fix for the chronic false LOWBATT/REPLBATT events reported by many users on APC Back-UPS BX-series units (BX750MI, BX950MI, BX1200MI, BX1600MI, BX2200MI and others in the same family) since 2024.
Full discussion, hardware reports and history: issue #2347.
usbhid-ups logs frequent nut_libusb_get_report/nut_libusb_get_interrupt I/O errors and/or spurious LOWBATT/REPLBATT notifications while the UPS is actually online, fully charged and healthy (ups.test.result: Done and passed). Frequency varies (a few times a day to well over 100/hour) and is not tied to any real power event.
Confirmed working on an APC Back-UPS BX950MI, USB, Debian 13 (trixie) / OpenMediaVault host, xHCI USB controller — tested across 5 different physical ports (front and back) to rule out a port-specific issue.
Run usbhid-ups (NUT 2.8.5 or newer) with both of the following driver flags:
usbhid-ups -a <upsname> -x lbrb_log_delay_sec=3 -x lbrb_log_delay_without_calibrating
Important gotcha: lbrb_log_delay_sec alone does nothing on this hardware family. Looking at the driver source, lbrb_log_delay_sec is only honored while the UPS reports itself as calibrating -- and BXnnnnMI units apparently never report a calibrating state to the host, so without lbrb_log_delay_without_calibrating the delay logic is effectively dead code for this hardware. Both flags together are required.
Result on the tested unit: 0 I/O errors and 0 false LOWBATT/REPLBATT over more than a week of runtime, versus roughly 106 I/O errors/hour before. This was a larger improvement than the lbrb_log_delay_sec documentation alone suggested — the underlying I/O errors disappeared entirely too, possibly from general USB/libusb robustness improvements between 2.8.1 and 2.8.5 rather than this specific flag (a single field test can't fully separate the two).
Many distros (Debian stable, OpenMediaVault, TrueNAS SCALE/CORE, Proxmox) in 2026 still ship NUT 2.8.0 or 2.8.1, both affected. Two field-tested ways to run a newer usbhid-ups without breaking the distro package or replacing the whole NUT stack:
Keeps the apt package intact and upgradable; every other part of the stack (ups.conf, systemd units, shutdown path) stays untouched — only the usbhid-ups binary invocation is redirected.
-
Build NUT ≥ 2.8.5 from source, driver-only, without installing over the system paths:
./configure --prefix=/some/staging/path/never-installed \ --with-drivers=usbhid-ups --with-usb=libusb-1.0 \ --with-user=nut --with-group=nut --with-doc=skip \ --without-ssl --disable-static makeNever run
make install— copy the resultingusbhid-upsbinary out manually instead, e.g. to/usr/local/lib/nut-2.8.5/usbhid-ups. -
Divert the original packaged binary aside (keeps it recoverable, keeps the apt package's file list intact):
dpkg-divert --local --rename --add /usr/lib/nut/usbhid-upsThis moves the original to
/usr/lib/nut/usbhid-ups.distriband lets you place a new file at/usr/lib/nut/usbhid-ups. -
Put a small wrapper script at
/usr/lib/nut/usbhid-ups(the diverted path) that execs the source-built binary with the two flags baked in:#!/bin/sh # Local diversion -> NUT 2.8.5+ built from source, fixes chronic false # LOWBATT/REPLBATT on APC Back-UPS BX-series (see nut#2347). # Original packaged binary kept at /usr/lib/nut/usbhid-ups.distrib export NUT_CONFPATH=/etc/nut export NUT_STATEPATH=/run/nut export NUT_ALTPIDPATH=/run/nut exec /usr/local/lib/nut-2.8.5/usbhid-ups "$@" \ -x lbrb_log_delay_sec=3 -x lbrb_log_delay_without_calibrating
Make it executable (
chmod +x). Note: if the source build was configured with a--prefixthat was never actually installed to, the binary's compiled-in default config path won't exist — exportingNUT_CONFPATH/NUT_STATEPATH/NUT_ALTPIDPATHas above points it back at the distro's real paths instead. -
Restart the driver (
systemctl restart nut-driver@<name>or viaupsdrvctl/OMV's own service management). No changes needed toups.conf,upsmon.confor systemd units — they keep invoking/usr/lib/nut/usbhid-upsas before, which is now the wrapper.
Rollback, fully reversible, no trace left:
dpkg-divert --local --rename --remove /usr/lib/nut/usbhid-ups
For systems like TrueNAS SCALE where a local dpkg-divert isn't practical, @invario built a Docker-based alternative: truenas-ups-workaround. It builds the latest usbhid-ups from source inside a container, copies the binary to a host path outside the container, and adds a driverpath=/path/to/updated/drivers line at the top of ups.conf so upsdrvctl looks there instead of the packaged location. Older NUT server/client tooling connects to the updated driver without issue.
NUT 2.8.5 has an unrelated bug in usbhid-ups (and USB drivers generally): a loop coding error that can cause a SEGFAULT when reconnecting or searching for devices. Fixed on master (merge referenced as #3550) but not yet in a numbered release as of this writing. Worth being aware of if you build 2.8.5 from source for this fix -- consider building from a master checkout past that fix instead if you can, or keep an eye on the driver's stability (service restart count, journalctl -u 'nut-driver@*') after deploying.