feat: add NUT (Network UPS Tools) integration base#126
feat: add NUT (Network UPS Tools) integration base#126RoiArthurB wants to merge 3 commits intodevfrom
Conversation
* Install modern `nut-client` library. * Add default `.env` configuration (NUT_HOST, NUT_PORT, NUT_UPS_NAME). * Create `NutManager` to handle UPS connections and monitor `ONBATT`, `ONLINE`, and `LOWBATT` events. * Integrate `NutManager` into `Controller` initialization and restarts. * Add list of possible features to implement inside the stub handle methods of `NutManager`. * Gracefully warn and cleanup the class when no UPS is available. Co-authored-by: RoiArthurB <16764085+RoiArthurB@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
* Install modern `nut-client` library. * Add default `.env` configuration (NUT_HOST, NUT_PORT, NUT_UPS_NAME). * Create `NutManager` to handle UPS connections and monitor `ONBATT`, `ONLINE`, and `LOWBATT` events. * Verify `upsc` and `upsd` exist on the system if connecting to `localhost`. * Integrate `NutManager` into `Controller` initialization and restarts. * Add list of possible features to implement inside the stub handle methods of `NutManager`. * Gracefully warn and cleanup the class when no UPS is available. Co-authored-by: RoiArthurB <16764085+RoiArthurB@users.noreply.github.com>
* Install modern `nut-client` library. * Add default `.env` configuration (NUT_HOST, NUT_PORT, NUT_UPS_NAME). * Create `NutManager` to handle UPS connections and monitor `ONBATT`, `ONLINE`, and `LOWBATT` events. * Verify `upsc` and `upsd` exist on the system if connecting to `localhost`. * Integrate `NutManager` into `Controller` initialization and restarts. * Add list of possible features to implement inside the stub handle methods of `NutManager`. * Add explicit code example showing how to trigger a delayed UPS shutdown. * Gracefully warn and cleanup the class when no UPS is available. Co-authored-by: RoiArthurB <16764085+RoiArthurB@users.noreply.github.com>
|
|
After some long exploration detailed below, using NUT is not a usable solution on MacOS (it's a never ending fighting to get UPS' USB low-level access) and it's not maintainable in any way... See full exploration logNUT UPS Driver Setup on macOS — Full Conversation SummaryContextThis was a long troubleshooting session about getting an APC Back-UPS BX2200MI (USB vendor The Journey — Phase by PhasePhase 1: Initial NUT Setup (First Mac Mini — macOS 15.7.3)The first attempt to run NUT via Homebrew ( Problem A — Doubled PID path bug. NUT 2.8.4 from Homebrew had a compilation quirk where the PID file path got concatenated with itself: Problem B — USB Access Denied. macOS has its own built-in UPS daemon ( Resolution for the first Mac (15.7.3):
This worked. NUT ran, Phase 2: Replicating on a Second Mac Mini (macOS 15.7.4)Attempting the exact same setup on a second Mac mini running macOS 15.7.4 (one point release newer) failed spectacularly. New obstacle — AMFI (Apple Mobile File Integrity). The ad-hoc signed entitlement was rejected at runtime ( New obstacle — DriverKit
Attempted and failed approaches on 15.7.4:
Phase 3: The
|
| First Mac | Second Mac | |
|---|---|---|
| macOS version | 15.7.3 | 15.7.4 |
| UPS daemon | ioupsd (old, permanently disableable) | AppleUserHIDDrivers (DriverKit, respawns with dynamic ID) |
| SIP off sufficient? | Yes | No (also needs AMFI off) |
| NUT works? | ✅ Yes | ❌ No — IOKit matching is undefeatable |
Key Takeaways
- macOS on Apple Silicon is fundamentally hostile to third-party USB HID device control. Three independent security layers (SIP, AMFI, SSV/DriverKit) all conspire to prevent it.
- NUT's
usbhid-upsuseslibusb, and libusb's own FAQ explicitly says "do not use libusb with HID devices under macOS." - Version pinning is the correct M2L2 strategy — all units should ship on the same known-good macOS version (15.7.3) with updates permanently disabled.
- The
nut-clientnpm package works perfectly once NUT'supsdis running — the entire battle was about getting the USB driver layer working, not the network protocol layer. - APC UPS devices ignore shutdown commands while on AC power — this is normal hardware behavior, not a software bug.
Chat Link
# NUT UPS Driver Setup on macOS — Full Conversation SummaryContext
This was a long troubleshooting session about getting an APC Back-UPS BX2200MI (USB vendor 051d:0002) to be controllable from Node.js via the nut-client npm package on macOS Apple Silicon Mac minis, as part of the M2L2 deployment box in the SIMPLE project. The goal: read UPS status and send shutdown commands (e.g. "shut down in 5 minutes") programmatically.
The Journey — Phase by Phase
Phase 1: Initial NUT Setup (First Mac Mini — macOS 15.7.3)
The first attempt to run NUT via Homebrew (brew install nut) hit two immediate problems:
Problem A — Doubled PID path bug. NUT 2.8.4 from Homebrew had a compilation quirk where the PID file path got concatenated with itself: /opt/homebrew/var/state/ups//opt/homebrew/var/state/ups/usbhid-ups-myups.pid.pid. Fix was to ensure the state directory existed (mkdir -p /opt/homebrew/var/state/ups) and leave the config defaults alone.
Problem B — USB Access Denied. macOS has its own built-in UPS daemon (com.apple.ioupsd) that exclusively claims APC USB devices via IOKit. NUT's usbhid-ups driver (which uses libusb) couldn't claim the device even as root.
Resolution for the first Mac (15.7.3):
- Disable SIP (boot into Recovery →
csrutil disable) - Disable
ioupsdviasudo launchctl disable system/com.apple.ioupsd+ reboot - Sign the
usbhid-upsbinary with thecom.apple.vm.device-accessentitlement (ad-hoc codesigning) - Run the driver in foreground mode (
-FFflag) — daemonizing lost the USB claim - Set
allow_killpower = 1inups.conf - Discover that APC UPS shutdown commands only work when the UPS is on battery (not on AC power)
This worked. NUT ran, upsd served on localhost:3493, and nut-client from Node.js could read status and send shutdown.default commands.
Phase 2: Replicating on a Second Mac Mini (macOS 15.7.4)
Attempting the exact same setup on a second Mac mini running macOS 15.7.4 (one point release newer) failed spectacularly.
New obstacle — AMFI (Apple Mobile File Integrity). The ad-hoc signed entitlement was rejected at runtime (zsh: killed). The first Mac on 15.7.3 didn't need AMFI disabled; 15.7.4 tightened validation. Fix: sudo nvram boot-args="amfi_get_out_of_my_way=1" + reboot + re-sign the binary.
New obstacle — DriverKit AppleUserHIDDrivers. Even with ioupsd disabled and AMFI off, a new macOS service appeared: com.apple.driverkit.AppleUserHIDDrivers-0x<dynamic_hex>. This DriverKit user-space HID driver replaced the old ioupsd mechanism for claiming USB UPS devices. Key problems:
- The service ID changes on every reboot (dynamic hex suffix), so
launchctl disablecan't target it persistently launchctl bootoutcould kill it, but it respawns immediately- Repeatedly killing it crashed the Mac (it also handles keyboard/mouse HID)
- The IOKit matching system holds an active driver instance on the device that can't be unloaded from userspace
Attempted and failed approaches on 15.7.4:
kextunloadofIOHIDPowerSource— blocked by reference fromAppleBTMkext which has an active IORegistry match- Dynamic
bootoutscript with race-condition timing — crashed the Mac macosx-upsNUT driver (uses Apple'sIOPowerSourcesAPI) — works for monitoring only, shutdown commands are hardcoded as unsupported in the source code- UTM/QEMU Linux VM with USB passthrough — macOS claims the device before the VM sees it
- USB/IP forwarding — no mature macOS tools
Phase 3: The apcupsd Detour
Since NUT/libusb was fundamentally blocked on 15.7.4, we tried apcupsd which has a native macOS IOKit HID backend (doesn't need libusb):
Problem — kext installation blocked. apcupsd needs ApcupsdDummy.kext installed in /System/Library/Extensions/, but:
- The system volume is a read-only APFS sealed snapshot (SSV) on Apple Silicon
- Even with SIP off,
mount -uw /fails with "Permission denied" - Would need
csrutil authenticated-root disableon top ofcsrutil disable - Even then, the kext "does not work as expected" per brew's own caveats — requires physical USB replug after every reboot
apcupsd capabilities if it worked:
- Read status: yes, via
node-apcaccessnpm over NIS (port 3551) - Send commands: no network interface — must shell out to
apccontrol/apcupsd --killpower - Has built-in graceful shutdown flow with configurable timing (
ONBATTERYDELAY,BATTERYLEVEL,MINUTES,KILLDELAY)
Phase 4: Root Cause Identification
The breakthrough was comparing macOS versions:
| First Mac | Second Mac | |
|---|---|---|
| macOS version | 15.7.3 | 15.7.4 |
| UPS daemon | ioupsd (old, permanently disableable) |
AppleUserHIDDrivers (DriverKit, respawns with dynamic ID) |
| SIP off sufficient? | Yes | No (also needs AMFI off) |
| NUT works? | ✅ Yes | ❌ No — IOKit matching is undefeatable |
Apple deliberately tightened DriverKit UPS handling in macOS 15.7.4. No search results or NUT GitHub issues (#2827, #2642) found a workaround. Both issues remain open with no fixes.
Final Conclusions
For macOS 15.7.3 — NUT Works
The complete working setup requires:
- SIP disabled (
csrutil disablein Recovery) - AMFI disabled (
sudo nvram boot-args="amfi_get_out_of_my_way=1") — needed on some builds ioupsddisabled (sudo launchctl disable system/com.apple.ioupsd+ reboot)- Binary signed with
com.apple.vm.device-accessentitlement - Driver run in foreground (
-FFflag) allow_killpower = 1inups.conf- Shutdown commands only work on battery (APC hardware behavior)
Node.js integration via nut-client on localhost:3493 provides full read+write access including shutdown.default.
For macOS 15.7.4+ — No Solution Exists
The recommended path is to downgrade to macOS 15.7.3 via IPSW restore (Apple Configurator 2 or DFU mode) and pin the OS version with automatic updates disabled. This is the correct architecture for M2L2 as a fixed deployment appliance.
Alternative Architectures Discussed (All Rejected for M2L2)
| Alternative | Why rejected |
|---|---|
| Hermes Pi as NUT host | Out of scope for M2L2 deployment |
| OpenWrt on router | M2L2 uses TP-Link Deco (no OpenWrt) |
| Android TV box as USB host | Not explored fully, but possible |
pmset from Node.js |
Read-only — can't send UPS commands |
macosx-ups driver |
Monitoring only — shutdown hardcoded as unsupported |
apcupsd on macOS |
Kext install blocked by SSV; no network command interface |
| Linux VM with USB passthrough | Host claims device before VM sees it |
Key Takeaways
- macOS on Apple Silicon is fundamentally hostile to third-party USB HID device control. Three independent security layers (SIP, AMFI, SSV/DriverKit) all conspire to prevent it.
- NUT's
usbhid-upsuseslibusb, and libusb's own FAQ explicitly says "do not use libusb with HID devices under macOS." - Version pinning is the correct M2L2 strategy — all units should ship on the same known-good macOS version (15.7.3) with updates permanently disabled.
- The
nut-clientnpm package works perfectly once NUT'supsdis running — the entire battle was about getting the USB driver layer working, not the network protocol layer. - APC UPS devices ignore shutdown commands while on AC power — this is normal hardware behavior, not a software bug.
Chat Link
Claude chat
Closing this PR is favor of exploring capabilities of node-hid only (limiting dependencies to nodejs only again, and relying on what MacOS offers)
Understood. Acknowledging that this work is now obsolete and stopping work on this task. Using |
- Using `node-hid` to communicate with the UPS - Detailed explanation can be found here : #126 (comment) - Add reverse engineered library to communicate exactly with APC Back-UPS BX2200MI - Add a test file to check communication with UPS



This PR introduces the base system for integrating UPS monitoring using Network UPS Tools (NUT). It leverages the modern
nut-clientpackage and itsMonitorclass to listen for UPS events such asONBATT,ONLINE, andLOWBATT.When the Controller initializes, it will instantiate
NutManager. The manager checks if the specified UPS is available; if not, it gracefully exits and cleans itself up. Stub functions have been added to log events and outline future implementations like executing server shutdowns or turning off connected VR headsets via ADB.Related #74
PR created automatically by Jules for task 11356153773324197732 started by @RoiArthurB