npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

apc-ups-hid

v0.1.2

Published

Direct HID control and monitoring of APC UPS devices from Node.js, without NUT or libusb.

Readme

apc-ups-hid

Read status from, and send commands to, APC UPS devices directly over USB HID from Node.js.

It talks to the UPS through node-hid (hidapi), so it needs neither NUT (Network UPS Tools) nor a libusb driver detach. On macOS that matters: hidapi goes through IOKit and coexists with the built-in DriverKit UPS driver instead of fighting it, so no SIP changes, AMFI flags, or entitlement signing are required.

This library is extracted from the SIMPLE Project's WebPlatform, where it powers the M2L2 portable classroom's power monitoring.

Status

Developed and tested against (only) an APC Back-UPS BX2200MI (vendorId 0x051d, productId 0x0002) on Linux, macOS and windows.

The HID report IDs are specific to this device family they were mapped from the NUT apc-hid.c source plus empirical probing. Other APC models are likely to work, but the report map may differ; see Verifying a new model.

Install

npm install apc-ups-hid

Node.js 20 or newer. node-hid is a native addon: it ships prebuilt binaries for common platforms and falls back to compiling from source (which needs a C++ toolchain and, on Linux, libudev-dev).

Linux permissions

hidraw devices are root-only by default. To use the UPS as a normal user, add a udev rule:

# /etc/udev/rules.d/50-apc-ups.rules
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="051d", ATTRS{idProduct}=="0002", MODE="0666"

Then reload and re-plug the device:

sudo udevadm control --reload-rules && sudo udevadm trigger

macOS and Windows need no special setup.

Quick start

import { ApcUpsHid } from "apc-ups-hid";

const ups = new ApcUpsHid(); // opens the first matching device

console.log(ups.info); // { manufacturer, product, serialNumber, path }
console.log(ups.getStatus()); // full snapshot, one call

ups.on("power-lost", () => console.log("Running on battery"));
ups.on("power-restored", () => console.log("AC is back"));
ups.on("battery-critical", (charge) => console.log(`Battery at ${charge}% - shutting down`));

ups.startPolling(5000); // emits 'status' every 5s and the transition events above

// later
ups.close();

CommonJS works too:

const { ApcUpsHid } = require("apc-ups-hid");

Listing devices without opening one

import { ApcUpsHid } from "apc-ups-hid";

for (const device of ApcUpsHid.listDevices()) {
	console.log(device.product, device.serialNumber);
}

API

new ApcUpsHid(options?)

| Option | Type | Default | Description | | ----------- | --------- | -------- | -------------------------------------------------------- | | vendorId | number | 0x051d | USB vendor ID (APC). | | productId | number | 0x0002 | USB product ID. | | autoOpen | boolean | true | Open the device in the constructor. false defers to open(). |

open() throws if no matching device is present, so with autoOpen: true the constructor itself can throw.

Connection

| Member | Description | | --------------------------------------------- | ------------------------------------------------------------------ | | open(): void | Open the HID handle. No-op when already open. Emits connected. | | close(): void | Stop polling and release the handle. Emits disconnected. | | isConnected: boolean | Whether the handle is currently open. | | info: UpsDeviceInfo \| null | Manufacturer, product, serial number and HID path. | | static listDevices(vendorId?, productId?) | Enumerate matching devices without opening them (deduplicated by path). |

Reading

| Method | Returns | Description | | ------------------------------- | ----------- | ------------------------------------------------------ | | getStatus() | UpsStatus | Every field below, read in one pass. | | getBatteryCharge() | number | Charge percentage, 0–100. | | getRuntimeSeconds() | number | Estimated runtime on battery, in seconds. | | isOnAC() | boolean | Whether mains power is present. | | getInputVoltage() | number | Input voltage (coarse, 1-byte report). | | getShutdownTimer() | number | Seconds until the armed shutdown, or -1 if not armed. | | getBeeperStatus() | BeeperState | "disabled" \| "enabled" \| "muted" \| "unknown". | | getStartupDelay() | number | Seconds the UPS waits before restoring output. |

UpsStatus fields:

| Field | Type | Description | | ---------------------- | ------------- | ------------------------------------------------------------ | | batteryCharge | number | Charge percentage, 0–100. | | runtimeSeconds | number | Estimated runtime remaining, in seconds. | | runtimeMinutes | number | Same value in minutes, rounded to one decimal. | | inputVoltage | number | Coarse input voltage. | | inputVoltagePrecise | number | Input voltage from the UPS ADC; drops to 0 on battery. | | acPresent | boolean | Mains power present. | | onBattery | boolean | Inverse of acPresent. | | shutdownTimer | number | Seconds until shutdown, -1 when not armed. | | beeperStatus | BeeperState | Audible alarm state. | | nominalPowerWatts | number | Configured active power, in watts. | | lowVoltageTransfer | number | Low transfer point, in volts. | | highVoltageTransfer | number | High transfer point, in volts. | | fullChargeCapacity | number | Full-charge capacity percentage. | | timestamp | number | Date.now() at the time of the read. |

Commands

| Method | Description | | ---------------------------- | ---------------------------------------------------------------------------------- | | shutdown(delaySeconds) | Arm the shutdown timer (1–32767 s). Throws outside that range. | | cancelShutdown() | Disarm it (writes -1). | | setBeeper(state) | "enabled", "muted" or "disabled". Throws on anything else. | | setStartupDelay(seconds) | Seconds to wait after AC returns before restoring output. |

APC devices ignore shutdown() while on AC power. The timer is only honoured when the UPS is running on battery. Arming it on mains and expecting the output to cut is the most common surprise with this hardware.

Polling and events

startPolling(intervalMs = 5000) polls immediately, then on every interval. The first poll establishes the baseline, so no transition event fires for it. stopPolling() stops; isPolling reports the current state.

| Event | Payload | Fired when | | ------------------ |------------------| ----------------------------------------------------------------- | | status | UpsStatus | Every poll tick. | | power-lost | - | acPresent goes true → false. | | power-restored | - | acPresent goes false → true. | | battery-low | charge: number | On battery, charge crosses down through 20%. | | battery-critical | charge: number | On battery, charge crosses down through 10%. | | connected | UpsDeviceInfo | The device is opened. | | disconnected | - | The device is closed (or lost). | | error | Error | A read fails mid-poll. Polling continues; it does not throw. |

Battery events fire once per crossing, not on every tick below the threshold, and never while on AC power. A drop past both thresholds in a single tick emits battery-critical only.

A read error usually means the USB cable was pulled. A robust consumer closes and retries:

ups.on("error", () => {
	ups.close(); // triggers 'disconnected'
});
ups.on("disconnected", () => setTimeout(() => ups.open(), 5000));

HID report map

Read with getFeatureReport(id, length); the first byte of the response is the report ID itself. Multi-byte values are little-endian.

| Report | ID | Bytes | Meaning | | ------ | ------ | ----- | ---------------------------------------------- | | REMAINING_CAPACITY | 0x0c | 1 | Battery charge, 0–100%. | | FULL_CHARGE_CAPACITY | 0x0d | 1 | Full-charge capacity, 0–100%. | | DESIGN_CAPACITY | 0x0e | 1 | Design capacity, 0–100%. | | RUNTIME_TO_EMPTY | 0x0f | 2 | Runtime remaining, seconds. | | AC_PRESENT | 0x13 | 1 | 1 = mains present, 0 = on battery. | | DELAY_BEFORE_SHUTDOWN | 0x15 | 2 | Signed: -1 not armed, >0 seconds. | | DELAY_BEFORE_STARTUP | 0x16 | 2 | Seconds. | | DELAY_BEFORE_REBOOT | 0x17 | 2 | Seconds. | | INPUT_VOLTAGE | 0x30 | 1 | Volts, approximate. | | INPUT_VOLTAGE_PRECISE | 0x31 | 2 | Volts; reads 0 on battery. | | LOW_VOLTAGE_TRANSFER | 0x32 | 2 | Volts. | | HIGH_VOLTAGE_TRANSFER | 0x33 | 2 | Volts. | | CONFIG_ACTIVE_POWER | 0x52 | 2 | Nominal output power, watts. | | AUDIBLE_ALARM_CONTROL | 0x78 | 1 | 1 disabled, 2 enabled, 3 muted. | | PRESENT_STATUS | 0x07 | var | Status bitfield. Not decoded by this library. |

Development

npm install
npm test        # hermetic: node-hid is mocked, no hardware needed
npm run typecheck
npm run lint
npm run build   # dist/esm + dist/cjs, with .d.ts

Fixtures

The suite replays recorded HID reports through a fake device, so it runs in CI. Two fixture kinds live in test/fixtures/:

  • *.json a single snapshot (reports plus the expected decode). Every file with a non-empty expected block is picked up automatically by the decode suite.
  • *.trace.json a series of samples over time. The trace suite replays them through startPolling() and checks the emitted power events against an independent count of the AC transitions in the recording.

Both are produced from real hardware with:

npm run capture -- ac-power                              # snapshot
npm run capture -- power-pull --trace 60 --interval 2000 # trace; pull the plug while it runs

Drop the resulting file in test/fixtures/ and it is tested automatically, no edit to the test file needed.

Verifying a new model

  1. Plug the UPS in and run npm run hardware-check. It enumerates the device, reads every field, exercises the beeper, and polls for a few ticks. The shutdown step is commented out on purpose.
  2. If the model uses a different product ID, pass it: new ApcUpsHid({ productId: 0x0003 }), and npm run capture -- my-model --product 0x0003.
  3. Capture a snapshot, check the decoded values against the UPS's own display, and open a PR with the fixture.