- Python 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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> |
||
| docs | ||
| eatonups | ||
| udev | ||
| .gitignore | ||
| README.md | ||
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, 0x15–0x1A, 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