Deploy Network UPS Tools to monitor the server-room UPS units and feed data to
Home Assistant for power-usage tracking.
APC Smart-UPS C 1500 is fully operational: battery charge, runtime, load,
voltage, and status all reporting via usbhid-ups + APC HID 0.100 subdriver.
upsd listens on Tailscale (100.121.189.98:3493) for HA polling; local upsmon
provides graceful hypervisor shutdown on battery-low.
Key deployment fix: the udev rule must cover SUBSYSTEM=="usb" (raw
/dev/bus/usb) in addition to hidraw, because usbhid-ups opens the raw USB
device file after dropping to the nut user via setuid(). Matching only hidraw
causes EACCES on driver start.
Tripp Lite UPS (09ae:3016) is blocked — driver matches TrippLite HID 0.85
subdriver but fails reading the 878-byte HID Report Descriptor (EAGAIN via USB
hub). Needs physical reseat to direct motherboard USB port. Driver masked to
prevent restart-loop spam.
Files: ups/discover.sh, ups/setup.sh (idempotent), ups/status.sh, ups/README.md
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
UPS Management (NUT — Network UPS Tools)
Centralized UPS monitoring for the server room via
NUT, running on pfv-tsys1. USB HID UPS
units feed one upsd network server; Home Assistant polls it over Tailscale for
real-time power/load/runtime tracking, and a local upsmon shuts the hypervisor
down gracefully when battery is low.
Why NUT (not apcupsd)? Two different UPS brands (APC + Tripp Lite) must be covered.
apcupsdonly supports APC, so it would require a second daemon stack. NUT'susbhid-upsdriver speaks to both via the USB HID Power Device class, and Home Assistant ships a first-class NUT integration.
Hardware
| UPS | Model | VID:PID | USB Serial | Status |
|---|---|---|---|---|
| APC | Smart-UPS C 1500 (FW 02.2) | 051d:0003 |
AS1213210423 |
LIVE |
| Tripp Lite | UPS (HID PDC) | 09ae:3016 |
2352CVLSM871900694 |
Blocked — see below |
Current State (2026-07-30)
APC Smart-UPS C 1500 — OPERATIONAL
Fully reporting via usbhid-ups + APC HID 0.100 subdriver. Data validated:
battery.charge: 100 battery.runtime: 1800 battery.voltage: 27.4
ups.status: OL ups.load: (via HA) ups.model: Smart-UPS C 1500
Tripp Lite UPS — BLOCKED (hardware issue)
The driver finds the device, matches the TrippLite HID 0.85 subdriver, claims
the interface, and reads the HID descriptor — but fails reading the 878-byte
HID Report Descriptor (Resource temporarily unavailable / EAGAIN after 5s).
The driver is masked to prevent restart-loop spam.
USB descriptors (manufacturer, product, serial) are readable via lsusb -v and
nut-scanner, but the bulk control transfer for the full report descriptor
times out. Likely causes:
- USB hub — the Tripp Lite is behind a Genesys Logic hub (
05e3:0608). Try plugging directly into a motherboard USB port. - USB cable — try a high-quality data cable (not charge-only).
- UPS firmware — the USB controller may not properly implement all HID endpoints.
To retry after physical reseat:
# On pfv-tsys1:
systemctl unmask nut-driver@tripp-lite-ups
systemctl start nut-driver@tripp-lite-ups
upsc tripp-lite-ups@localhost
Architecture
pfv-tsys1 (192.168.3.11 / Tailscale 100.121.189.98)
├─ APC Smart-UPS C 1500 ──┐
└─ Tripp Lite UPS (masked) ──┤ USB HID
▼
nut-driver@apc-smartups-c1500 (usbhid-ups)
▼
upsd :3493 (LISTEN 127.0.0.1 + Tailscale)
▼ ▼
upsmon (local) Home Assistant (NUT integration)
graceful shutdown power/load/runtime sensors
- Driver layer —
usbhid-upsprocess, pinned by USB serial. Debian uses templatednut-driver@<upsname>.serviceunits managed bynut-driver-enumerator. - Server layer —
upsdexposes UPS data on TCP 3493 (localhost + Tailscale only). Clients authenticate viaupsd.users. - Monitor layer —
upsmonruns locally asmasterto triggerSHUTDOWNCMD(/sbin/shutdown -h now) when a UPS reportsLOWBATT. - Home Assistant — native NUT integration connects to
upsdover Tailscale and exposesups.load,battery.runtime,ups.status, etc. as sensors.
Key deployment lesson: udev must cover raw USB devices
The usbhid-ups driver opens /dev/bus/usb/BBB/DDD (raw USB device files),
not /dev/hidraw*. After calling setuid(111) to drop to the nut user,
it needs write access to those raw USB files. The udev rule must match
SUBSYSTEM=="usb" by vendor/product ID to set GROUP="nut" — matching only
hidraw is insufficient. See /etc/udev/rules.d/99-nut-ups.rules.
Scripts
All scripts run on the target host (pfv-tsys1) via tests/remote.sh:
# Idempotent install + configure (safe to re-run):
PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file ups/setup.sh
# Discover USB UPS + NUT state (read-only diagnostic):
PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file ups/discover.sh
# Query UPS data + service health:
PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file ups/status.sh
setup.sh accepts environment overrides for serials/VIDs/PIDs/usernames, so it
can be repurposed for other hosts or UPS units. Passwords for monuser and
homeassistant are auto-generated on first run and reused on subsequent runs
(stored in /etc/nut/upsd.users).
Set TRIPP_ENABLED=0 to skip the Tripp Lite entirely (useful if it's physically
unplugged).
Configuration files on pfv-tsys1
| File | Purpose |
|---|---|
/etc/udev/rules.d/99-nut-ups.rules |
Grant nut group rw on raw USB + hidraw devices (both subsystems) |
/etc/nut/ups.conf |
usbhid-ups device(s), pinned by serial + subdriver |
/etc/nut/upsd.conf |
LISTEN 127.0.0.1 + LISTEN <tailscale> on port 3493 |
/etc/nut/upsd.users |
monuser (master) + homeassistant (read-only) credentials |
/etc/nut/upsmon.conf |
Local master monitor + SHUTDOWNCMD |
/etc/nut/nut.conf |
MODE=netserver |
Home Assistant integration
In Home Assistant → Settings → Devices & Services → Add Integration → NUT:
| Field | Value |
|---|---|
| Host | pfv-tsys1 (or 100.121.189.98) over Tailscale |
| Port | 3493 |
| Username | homeassistant |
| Password | (printed by setup.sh / stored in /etc/nut/upsd.users) |
| UPS | apc-smartups-c1500 |
HA auto-discovers available UPS names after authentication. Useful sensors for
power-usage tracking: sensor.apc_smartups_c1500_ups_load (Watts, via ups.load
× VA rating), sensor.apc_smartups_c1500_battery_runtime,
sensor.apc_smartups_c1500_ups_realpower.
Daily operations
From pfv-tsys1 (or any tailnet host with NUT client installed):
# List UPS units served by upsd
upsc -l pfv-tsys1
# Full variable dump for one UPS
upsc apc-smartups-c1500@pfv-tsys1
# Just the live load + runtime
upsc apc-smartups-c1500@pfv-tsys1 ups.load
upsc apc-smartups-c1500@pfv-tsys1 battery.runtime
Notes
- No USB passthrough to the HA VM. Keeping the UPS on the host preserves
hypervisor graceful-shutdown capability and matches the
powerman/pattern (PDU managed on the host where the adapter physically lives). - Per-unit power (Watts):
usbhid-upsreportsups.realpowerdirectly on the APC Smart-UPS C 1500. If absent, derive Watts asload% × VA_rating × power_factorin HA templates.