Files
PFVCluster/ups
mrcharles 5456c783c6 feat(ups): NUT setup for APC Smart-UPS C 1500 on pfv-tsys1
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
2026-07-30 09:34:05 -05:00
..

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. apcupsd only supports APC, so it would require a second daemon stack. NUT's usbhid-ups driver 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:

  1. USB hub — the Tripp Lite is behind a Genesys Logic hub (05e3:0608). Try plugging directly into a motherboard USB port.
  2. USB cable — try a high-quality data cable (not charge-only).
  3. 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 layerusbhid-ups process, pinned by USB serial. Debian uses templated nut-driver@<upsname>.service units managed by nut-driver-enumerator.
  • Server layerupsd exposes UPS data on TCP 3493 (localhost + Tailscale only). Clients authenticate via upsd.users.
  • Monitor layerupsmon runs locally as master to trigger SHUTDOWNCMD (/sbin/shutdown -h now) when a UPS reports LOWBATT.
  • Home Assistant — native NUT integration connects to upsd over Tailscale and exposes ups.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-ups reports ups.realpower directly on the APC Smart-UPS C 1500. If absent, derive Watts as load% × VA_rating × power_factor in HA templates.