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.

Symptom

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.

Fix

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).

Deploying this on a distro stuck on an old packaged NUT (Debian/OMV/TrueNAS and similar)

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:

Option A — local dpkg-divert wrapper (Debian/OMV, apt-based systems)

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.

  1. 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
    make
    

    Never run make install — copy the resulting usbhid-ups binary out manually instead, e.g. to /usr/local/lib/nut-2.8.5/usbhid-ups.

  2. 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-ups
    

    This moves the original to /usr/lib/nut/usbhid-ups.distrib and lets you place a new file at /usr/lib/nut/usbhid-ups.

  3. 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 --prefix that was never actually installed to, the binary's compiled-in default config path won't exist — exporting NUT_CONFPATH/NUT_STATEPATH/NUT_ALTPIDPATH as above points it back at the distro's real paths instead.

  4. Restart the driver (systemctl restart nut-driver@<name> or via upsdrvctl/OMV's own service management). No changes needed to ups.conf, upsmon.conf or systemd units — they keep invoking /usr/lib/nut/usbhid-ups as before, which is now the wrapper.

Rollback, fully reversible, no trace left:

dpkg-divert --local --rename --remove /usr/lib/nut/usbhid-ups

Option B — containerized build + driverpath override (TrueNAS and similar appliance-style systems)

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.

Known caveat on 2.8.5 specifically

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.

⚠️ **GitHub.com Fallback** ⚠️