Repo containing Eaton UPS USB protocol research and tools
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
nak c0261b2f1d Add standalone HID tooling for Eaton/MGE UPS (0463:ffff)
The device is not serial: it is a standard USB HID Power Device Class UPS
whose 1173-byte report descriptor fully self-describes its data model via
usage pages 0x84 (Power Device) and 0x85 (Battery System). These tools parse
that descriptor from sysfs to build the field map, then read and write reports
with the hidraw ioctls. Stdlib only, nothing hardcoded per model.

- descriptor.py: HID report descriptor parser + Power/Battery usage tables
- device.py: hidraw ioctls, bit extraction, UPS class
- cli.py: describe / status / diag / dump / watch / log / set
- udev rule for non-root access (needs --action=add to apply to a live node)
- docs/PROTOCOL.md: report map and empirically verified scaling

Scaling is keyed off usage names and collection paths rather than the
descriptor's Unit items, which this firmware emits truncated and with bogus
exponents (voltage declared as watts, x10^7). Voltage is context-dependent:
decivolts under PowerSummary (battery pack), whole volts under PowerConverter.

Also documents that RunTimeToEmpty is modelled from a load/charge table rather
than measured, so it cannot be used to assess battery health.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 21:55:13 -07:00
docs Add standalone HID tooling for Eaton/MGE UPS (0463:ffff) 2026-08-08 21:55:13 -07:00
eatonups Add standalone HID tooling for Eaton/MGE UPS (0463:ffff) 2026-08-08 21:55:13 -07:00
udev Add standalone HID tooling for Eaton/MGE UPS (0463:ffff) 2026-08-08 21:55:13 -07:00
.gitignore Add standalone HID tooling for Eaton/MGE UPS (0463:ffff) 2026-08-08 21:55:13 -07:00
README.md Add standalone HID tooling for Eaton/MGE UPS (0463:ffff) 2026-08-08 21:55:13 -07:00

eaton-ups-usb

Standalone, dependency-free Python tools for Eaton / MGE UPS Systems devices that speak USB HID Power Device Class (0463:ffff).

Developed against an Eaton 5S1000LCD (which reports iProduct = "Ellipse PRO").

There is no proprietary protocol here

The device is not serial and needs no reverse engineering in the usual sense. It exposes a 1173-byte HID report descriptor that fully self-describes its data model using the standard usage pages:

  • 0x84 — Power Device Page (Voltage, Frequency, PercentLoad, DelayBeforeShutdown, …)
  • 0x85 — Battery System Page (RemainingCapacity, RunTimeToEmpty, Charging, …)
  • 0xFFFF — a handful of Eaton vendor-defined items

So these tools parse the descriptor straight from sysfs, build a field map from it, and then read/write reports with the hidraw ioctls. Nothing is hardcoded per model — point it at any HID Power Device UPS and it should decode.

Setup

No Python packages required (stdlib only). You just need access to the hidraw node:

sudo cp udev/99-eaton-ups.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules
sudo udevadm trigger --action=add --subsystem-match=hidraw

--action=add matters: udev only applies node ownership and mode on add events, so the default change action reloads rules without fixing the permissions on an already-present device.

Then confirm the node is group-readable:

ls -l /dev/hidraw*

Usage

python3 -m eatonups.cli describe      # decode the report descriptor (no device access needed)
python3 -m eatonups.cli describe -v   # ...including the raw HID item stream
python3 -m eatonups.cli status        # human-readable summary
python3 -m eatonups.cli dump          # read every feature report, decoded
python3 -m eatonups.cli dump -r 0x0E  # just one report
python3 -m eatonups.cli diag          # why-did-it-shut-down diagnostics
python3 -m eatonups.cli watch         # stream interrupt IN reports (state changes)
python3 -m eatonups.cli log -i 5      # poll on an interval; use during a discharge test

Report map

Report Contents
0x01 PresentStatus: ACPresent, Charging, Discharging, Good, InternalFailure, Overload, ShutdownImminent, NeedReplacement
0x02 Outlet PresentStatus (Present, SwitchOn/Off) ×2
0x03 Charger / PowerConverter internal status
0x04 Input status: Boost, Buck, FrequencyOutOfRange, VoltageOutOfRange
0x06 RemainingCapacity (%), RunTimeToEmpty (32-bit seconds)
0x07 PercentLoad, battery Voltage
0x08 RemainingCapacityLimit — UPS cuts power below this
0x09 DelayBeforeShutdown (1 = inactive)
0x0A DelayBeforeStartup
0x0B ID map (FlowID / OutletID / PowerSummaryID …)
0x0C Switchable, CapacityMode, DesignCapacity, FullChargeCapacity
0x0D ConfigApparentPower (nameplate VA), ConfigFrequency
0x0E Input Frequency/Voltage, Output ActivePower/ApparentPower/Frequency/Voltage
0x10 String indices (iManufacturer, iProduct, iSerialNumber, iDeviceChemistry)
0x13 / 0x14 High / LowVoltageTransfer thresholds
0x1F / 0x20 AudibleAlarmControl (beeper)
0x0F, 0x150x1A, 0x22, 0xFD, 0xFE Eaton vendor-defined

Run describe for exact bit offsets, sizes and logical ranges.

Caveat: the descriptor's unit fields are wrong

This firmware emits truncated Unit items with nonsense exponents — voltage is declared as cm^2*g*s^-3 (i.e. watts) with x10^7. This is a long-standing MGE quirk; NUT ignores HID units for exactly this reason and so do we. Scaling lives in SCALE in eatonups/device.py and is keyed off usage names, verified against real readings.

Layout

eatonups/descriptor.py   HID report descriptor parser + Power/Battery usage tables
eatonups/device.py       hidraw ioctls, bit extraction, UPS class
eatonups/cli.py          command-line interface
udev/99-eaton-ups.rules  non-root access