Skip to content

ragtech: add optional battery runtime model (runtimecal/chargetime) and test.battery.stop - #3716

Open
ygbr wants to merge 13 commits into
networkupstools:masterfrom
ygbr:ragtech-runtime-model
Open

ygbr wants to merge 13 commits into
networkupstools:masterfrom
ygbr:ragtech-runtime-model

Conversation

@ygbr

@ygbr ygbr commented Sep 26, 2026 •

Copy link
Copy Markdown

The ragtech driver (family-10 firmware, PR #3447 / #3504) exposes no runtime, and its battery charge byte is voltage-based: it reads 100 % as soon as the charger floats the bank, drops sharply under inverter load and bounces back when the load falls. On my New Easy Pro 3200 VA (20NEP4061, CbU/Ti — the same line as #3500) the firmware still reported ~31 % when it raised LB, and then cut the output only ~60–65 s later (30 s countdown after the beeping alarm). With stock settings upsmon therefore gets about one minute to shut a host down.

This PR adds an opt-in runtime model to ragtech, modelled on nutdrv_qx's runtimecal, plus a small test.battery.stop instcmd.

Commits

  1. drivers/ragtech.c: add optional battery runtime model

    • new options: runtimecal (same semantics as nutdrv_qx), chargetime, battery_ah, extbatt_ah, obload_factor, runtime_low;
    • battery.runtime from a Peukert-style fit through the two runtimecal points, scaled by (battery_ah + extbatt_ah) / battery_ah for external battery modules;
    • battery.charge becomes the driver's state-of-charge estimate: integrated from the energy drawn on battery, recovering along chargetime on mains (80 % at chargetime, full at 1.5×); the firmware reading is kept in the standard battery.charge.approx;
    • on mains the prediction multiplies ups.load by obload_factor, because the inverter draws noticeably more than the mains path for the same equipment (×1.24 measured: 9.2 % → 11.4 %);
    • LB is raised on battery when the estimate drops below runtime_low (default 300 s), also published as battery.runtime.low; battery.capacity is published too;
    • the estimate is saved in the state path (every poll on battery, once a minute otherwise) and resumed on restart if younger than 1 h, so a driver restart during recharge doesn't jump back to the inflated voltage-based value;
    • man page: option docs + a "CALIBRATING THE RUNTIME MODEL" section with the procedure and measured values; NEWS.adoc entry; three words for nut.dict.
  2. drivers/ragtech.c: add test.battery.stop instcmd — maps the OEM cancelDischarge action (F_OPCHECKUP=0, clear bit 2 of reg 0x90) so a test.battery.start.deep can be aborted; gated by allow_shutdown like the other test/shutdown commands.

  3. docs: document Ragtech New Easy Pro 3200VA and the calibration procedure — adds the unit to data/driver.list.in (reports model id 18 → identified as Easy 2200 TI, needs va=3200), describes it in the man page's SUPPORTED HARDWARE (model-id collision, current scaling cross-checked against the front-panel display and register 0x8D, firmware LB → A00 alarm → 30 s countdown → output cut ~60 s after LB), and documents the step-by-step discharge/recharge procedure that was executed, how each parameter is derived, and the measured values.

  4. drivers/ragtech.c: average the runtime load over time, not polls — the load behind battery.runtime is now a time-based average (300 s time constant on mains, 15 s on battery, independent of pollinterval). On mains the runtime is a forecast, and short spikes on a rack of network gear were swinging it between ~1050 and ~1870 s within an afternoon at a steady 9–10 % base load.

Without runtimecal in ups.conf the driver behaves exactly as before.

Calibration data (New Easy Pro 3200 VA, internal 2× 12 V 9 Ah, firmware 1.0, reports model id 18 so va = 3200)

  • Discharge: full bank → firmware LB in 1796 s (29 min 56 s) at an average 11.4 % load (≈365 VA). The datasheet-derived model (80 min @ ~110 W / 20 min @ ~300 W) had been ~20 % pessimistic. After LB the battery voltage collapsed from 21.71 V to 19.83 V within ~66 s.
  • Recharge: bulk ~4.1 h at ~1.5 A average, absorption up to 28.4–28.5 V, switch to float (~26.8 V) at 6.4 h after mains returned → chargetime = 15300.
  • Resulting config (also the man page example):
    va = 3200
    runtimecal = 1796,11.4,5592,5.0
    chargetime = 15300
    obload_factor = 1.24
    
  • The driver's ups.load agreed with the front-panel load display (10–13 %) during the discharge, so the 2200 TI current scaling is adequate for this unit.

Testing

  • Built with -Wall -Wextra -Wformat=2 -Wshadow, no warnings (each commit separately); new doc lines pass aspell with docs/nut.dict; all added lines in C sources, man page and NEWS are plain ASCII.
  • make distcheck-light passes (Ubuntu 26.04, --with-libsystemd) on the final tree.
  • Running on Ubuntu 26.04 (NUT 2.8.4 upsd/upsmon + this driver from master) against the unit above since 2026-09-25: full discharge to LB, overnight recharge, driver restarts during recharge (estimate resumed from the state file), USB re-enumeration.

AI disclosure

This contribution was prepared with Claude Code using the Claude Opus 5.5 model (driver code, docs, calibration analysis). I reviewed the changes, ran the hardware tests and calibration on my unit, and take responsibility for the submission; the DCO sign-offs were added by me. Each commit carries a Co-Authored-By: Claude Opus 5.5 trailer.

cc @juslex (driver author) and @LanayAlves (#3500, same Easy Pro 3200 line) — calibration points from other units/battery configurations would be very welcome.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MArYHUmWt81RRXHGNqzznC

@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

A ZIP file with standard source tarball and another tarball with pre-built docs for commit 37b8e3c is temporarily available: NUT-tarballs-PR-3716.zip.

@ygbr

ygbr commented Sep 26, 2026

Copy link
Copy Markdown
Author

Calibration report — Ragtech New Easy Pro 3200 VA (20NEP4061, CbU/Ti)

Unit: Ragtech New Easy Pro 3200 VA, 10 outlets, automatic 115/127/220 V input (127 V mains here), 115 V sine output, part number 20NEP4061 (CbU/Ti), internal 2× 12 V 9 Ah (24 V), USB CDC-ACM 04d8:000a, firmware 1.0.
Identification: the firmware reports model id 18, so the driver identifies it as Easy 2200 TI (the family-10 table ends at 2200 VA; Ragtech's own Supervise devices.xml has no 3200 entry either). With va = 3200 → ups.power.nominal: 3200, ups.realpower.nominal: 2240. The 2200 TI current scaling proved adequate: ups.load 9.3 % vs firmware register 0x8D = 8 % (integer, floored) on mains, and 10–12 % vs the front-panel display's 10–13 % on battery.
Host: Ubuntu 26.04.1, NUT 2.8.4 upsd/upsmon + this driver built from master; ~295 VA of network gear (router, Wi-Fi AP, 5 fiber ONTs, Starlink, two small PCs, chargers).

Procedure

  1. Bank full: on mains ~20 h beforehand, battery.voltage 26.7–26.9 V (float).
  2. upsmon stopped and runtime_low = 0 (driver restarted) so the host would not shut down; normal load kept connected.
  3. Logger: upsc every 5 s on battery / 60 s on mains to CSV (status, load, current, voltages, firmware charge, driver charge/runtime, temperature), plus a watcher that would abort on firmware LB, battery.voltage ≤ 21.0 V (10.5 V/block), temperature ≥ 68 °C, or any alarm.
  4. Unplugged from the wall at 16:18:04; ran until the firmware raised LB; mains restored during the countdown.
  5. Recharge logged overnight until the charger went to float.

Discharge (datasheet-seeded model runtimecal = 1200,13.4,4800,4.9 active during the test)

min ups.load % Vbat fw charge % driver charge % temp °C
0 12.3 25.46 100.0 99.6 50
3 11.3 25.06 86.3 85.4 55
6 12.3 24.92 84.3 73.9 58
9 15.1 24.66 80.8 62.2 59
12 10.4 24.39 75.3 50.4 62
15 11.3 23.99 71.0 39.0 64
18 11.3 23.58 66.7 26.5 62
21 10.3 23.32 61.6 12.9 62
24 13.2 23.18 54.5 1.2 63
27 10.4 22.91 50.2 0.0 64
29.9 9.4 21.71 31.0 0.0 63
  • Firmware LB at 16:48:00 → 1796 s, average 11.4 % on battery (≈365 VA; 9.2 % on mains before the test → obload_factor 1.24). The seeded model reached 0 at ~24.2 min (≈20 % pessimistic) → recalibrated to runtimecal = 1796,11.4,5592,5.0 (datasheet exponent 1.378 kept).
  • After LB: beeping, display A00, 30 s countdown; battery.voltage fell 21.71 → 19.83 V within ~66 s while the firmware charge byte went 31 → 2.7 %; mains restored at ~16:49:08, just before cutoff.
  • Output held 114.3–116.0 V / 60.05 Hz throughout; nothing on the load rebooted. Internal temperature peaked at 64–65 °C (fan cycles at ~63 °C).
  • Energy check: ≈140 Wh drawn from a 216 Wh nameplate bank at ~12 A (~1.3 C) ≈ 65 %, normal for healthy 9 Ah SLA at a 30-minute rate.

Recharge

h after mains Vbat fw charge % phase
0.0 23.85 4.3 bulk
1.0 25.59 41.6 bulk
2.0 26.26 55.7 bulk
4.1 27.05 ~70 absorption starts
6.3 28.54 (peak) 100.0 absorption
6.4 27.2 → 26.9 100.0 float (~26.8 V)
  • Bulk ≈ 5.9 Ah × 1.1 / 4.1 h ≈ 1.5 A average → a small charger (relevant for external modules: ~2 days to refill a ~90 Ah bank).
  • Full at 6.4 h → chargetime = 15300 (full = 1.5 × chargetime). With the old default (28800) the driver still showed 82 % half an hour after the bank was full. Periodic top-ups back to ~28.4 V occur while floating.
  • A driver restart during the recharge resumed the saved estimate (14.7 %) instead of the firmware's 27.8 %.

Final settings in production

va = 3200
runtimecal = 1796,11.4,5592,5.0
chargetime = 15300
battery_ah = 9
extbatt_ah = 0
obload_factor = 1.24
runtime_low = 300

ygbr and others added 4 commits September 26, 2026 18:11
The Ragtech family-10 firmware reports no runtime, and its battery
charge byte is derived from battery voltage alone: it reads 100 % as
soon as the charger floats the bank, drops sharply under inverter load
and recovers when the load falls. On an Easy Pro 3200 VA it still read
about 30 % when the firmware raised low battery, and the firmware cuts
the output only about one minute after that (30 s countdown), which is
too late for a clean host shutdown.

Add an opt-in runtime model, enabled by "runtimecal" (same semantics as
in nutdrv_qx), with "chargetime", "battery_ah", "extbatt_ah",
"obload_factor" and "runtime_low" options:

* battery.runtime from a Peukert-style fit through the two runtimecal
  points, scaled for external battery modules;
* battery.charge becomes the driver's own state-of-charge estimate,
  integrated from the energy drawn on battery and recovering along
  chargetime on mains; the firmware value moves to battery.charge.approx;
* on mains the prediction scales ups.load by obload_factor, as the
  inverter draws noticeably more than the mains path (x1.24 measured);
* LB is raised on battery once the estimate drops below runtime_low;
* the estimate is persisted in the state path so driver restarts (config
  changes, USB re-enumeration) do not reset it to the inflated
  voltage-based reading while charging.

Without runtimecal the driver behaves as before. Calibrated end-to-end on
a New Easy Pro 3200 VA (20NEP4061, internal 2x 12 V 9 Ah): 1796 s to
firmware LB at 11.4 % average load, full recharge in 6.4 hours; the
procedure and values are documented in the man page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MArYHUmWt81RRXHGNqzznC
Signed-off-by: Ygor Lemos <ygbr@mac.com>
Map the OEM "cancelDischarge" action from Ragtech's devices.xml
(F_OPCHECKUP=0, i.e. clear bit 2 of register 0x90) to
test.battery.stop, so a deep battery test started with
test.battery.start.deep can be aborted. Like the other test and
shutdown instcmds it is only exposed when allow_shutdown is set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MArYHUmWt81RRXHGNqzznC
Signed-off-by: Ygor Lemos <ygbr@mac.com>
* data/driver.list.in: add the Ragtech New Easy Pro 3200VA (20NEP4061,
  CbU/Ti), which reports model id 18 and is identified as an
  Easy 2200 TI, so it needs va=3200.
* docs/man/ragtech.txt: describe that unit under SUPPORTED HARDWARE
  (model id collision, current scaling cross-checked against the
  front-panel display and register 0x8D, firmware LB -> A00 alarm ->
  30 s countdown -> output cut about 60 s after LB), and replace the
  short calibration note with the step-by-step discharge and recharge
  procedure that was executed on it, how each parameter is derived and
  the measured values.
* docs/nut.dict: add CbU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MArYHUmWt81RRXHGNqzznC
Signed-off-by: Ygor Lemos <ygbr@mac.com>
The load behind battery.runtime was an exponential average over the
last few polls (about 15 s at the default pollinterval). On mains,
where the figure is a forecast of an outage starting now, short load
spikes on a rack of network gear (a phone charger, a fan, a PC waking
up) swung it by several minutes between polls: on the Easy Pro 3200 VA
the displayed runtime moved between about 1050 and 1870 s within an
afternoon at a steady 9-10 % base load.

Make the average time-based (independent of pollinterval) with a time
constant of 300 s on mains and 15 s on battery, where the figure has to
follow the real discharge. The state of charge integration still uses
the instantaneous load.

AI disclosure: prepared with Claude Code (Claude Opus 5.5); reviewed,
tested on the hardware and submitted by the human author.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MArYHUmWt81RRXHGNqzznC
Signed-off-by: Ygor Lemos <ygbr@mac.com>
@ygbr
ygbr force-pushed the ragtech-runtime-model branch from 7c0d60e to c97e0c4 Compare September 26, 2026 21:12
@jimklimov jimklimov added Incorrect or missing readings On some devices driver-reported values are systemically off (e.g. x10, x0.1, const+Value, etc.) Shutdowns and overrides and battery level triggers Issues and PRs about system shutdown, especially if battery charge/runtime remaining is involved Ragtech Devices made by Ragtech Sistemas de Energia enhancement AI For good or bad, machine tools are upon us. Humans are still the responsible ones. labels Sep 29, 2026
@jimklimov jimklimov added this to the 2.8.6 milestone Sep 29, 2026
@jimklimov

Copy link
Copy Markdown
Member

MAINTAINER NOTE: Cross-linking to #2420 for holistic addressing of this problem area, eventually.

Comment thread drivers/ragtech.c Fixed
Comment thread drivers/ragtech.c Fixed
Comment thread drivers/ragtech.c Dismissed

@jimklimov jimklimov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally LGTM, a docs update about regular saving of State-of-Charge files (and whether the sysadmin wants it to persist across reboots) would be desirable.

The runtimecal problem area (and potentially the driver option names involved) may later be impacted by #2420.

Comment thread drivers/ragtech.c Outdated
Address review of the runtime model:

* Create the state-of-charge file under a 022 umask, as writepid() does,
  so it is never group/world-writable (CodeQL "File created without
  restricting permissions").
* Add a soc_save_interval option (seconds, default 60; 0 disables both
  saving and resuming the estimate). The file is now written on that
  interval, on each mains/battery transition and on driver exit, instead
  of on every poll while on battery. A failing save is logged once.
* Document the file in a new STATE OF CHARGE FILE section of the man page:
  location in the state path, write frequency, storage wear and possible
  I/O stalls on persistent or flash storage, tmpfs vs persistent state
  paths and what happens across reboots, so the sysadmin can choose.
* Note that the calibration values in the man page were measured on a
  brand new unit with its factory batteries.
* Make the charge-time arithmetic use explicit double conversions.

Tested on a Ragtech New Easy Pro 3200VA: file created -rw-r--r--, written
every 60 s and on exit, nothing written with soc_save_interval = 0; builds
with --enable-warnings=hard --enable-Werror (gcc 15) and auto (clang 21),
ragtech.c also clean under clang hard.

AI disclosure: prepared with Claude Code (Claude Opus 5.5); reviewed,
tested on the hardware and submitted by the human author.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MArYHUmWt81RRXHGNqzznC
Signed-off-by: Ygor Lemos <ygbr@mac.com>
@ygbr

ygbr commented Oct 10, 2026

Copy link
Copy Markdown
Author

@jimklimov thanks for the review and for merging master in and fixing the NEWS references. I pushed 51e8f71 (on top of your merge) addressing it:

Changes

  • State-of-charge file
    • Created under a 022 umask like writepid(), which resolves the CodeQL world-writable alert.
    • New soc_save_interval option (default 60 s, 0 = no saving/resuming). The file is written on that interval, on mains/battery transitions and at exit. Previously it was written on every poll while on battery.
    • A failing save is logged once.
  • Docs
    • New STATE OF CHARGE FILE man page section covering location, write frequency, flash wear and possible I/O stalls, tmpfs vs persistent state paths and behaviour across reboots, so the sysadmin can decide.
    • The calibration example now states it was measured on a brand new unit with its factory batteries (calibrated within days of purchase), with a reminder to recalibrate as batteries age.
    • NEWS bullet updated in place; umask added to nut.dict.
  • Explicit double conversions in the charge-time arithmetic.

On #2420: runtimecal here keeps exactly the nutdrv_qx semantics. The extra knobs (chargetime, battery_ah/extbatt_ah, obload_factor, runtime_low, soc_save_interval) are local to ragtech for now, and I'm happy to rename or move them to whatever common pattern that refactor settles on.

Testing

On the Ragtech New Easy Pro 3200 VA (20NEP4061, Ubuntu 26.04):

  • The file is created -rw-r--r-- and rewritten every 60 s and on driver exit.
  • With soc_save_interval = 0 nothing is written, neither while running nor at exit.
  • Normal operation is unchanged.

Builds:

  • gcc 15 with --enable-warnings=hard --enable-Werror: clean.
  • clang 21 with --enable-warnings=auto --enable-Werror: clean, and ragtech.c is also clean under clang hard.
  • make distcheck-light incl. spellcheck passes, and all added lines are plain ASCII.

About the previous CI failures (on d53cbed)

As far as I could tell they were not caused by this change:

  • Jenkins: the farm reported "Got 4 unclassified recorded dynamatrix stage results". The failing stages were OpenIndiana/OmniOS/FreeBSD workers going offline mid-run (danutci-oi was marked offline: Connection was broken, danutci-oo ... Node is being removed, ClosedChannelException in cleanWs).
  • AppVeyor: the job built and installed ragtech.exe and its test suite passed (PASSED=32 FAILED=0). The log then ends during the final 7z packaging step at ~56 minutes, which looks like a job time limit.

I couldn't read the farm's build logs directly (login required), so please tell me if something in them points at ragtech.c.

Calibration data for the unit is in the earlier comment. The discharge and recharge were measured on the brand-new, factory-fresh unit described above.


AI disclosure: prepared with Claude Code (Claude Opus 5.5); reviewed, hardware-tested and signed off by me.

Comment thread drivers/ragtech.c Dismissed
@github-actions github-actions Bot deleted a comment from AppVeyorBot Oct 14, 2026
@github-actions github-actions Bot deleted a comment from AppVeyorBot Oct 14, 2026
@github-actions github-actions Bot deleted a comment from AppVeyorBot Oct 14, 2026
@github-actions github-actions Bot deleted a comment from AppVeyorBot Oct 14, 2026
@github-actions github-actions Bot deleted a comment from AppVeyorBot Oct 14, 2026
@github-actions github-actions Bot deleted a comment from AppVeyorBot Oct 14, 2026
@github-actions github-actions Bot deleted a comment from AppVeyorBot Oct 14, 2026
@github-actions github-actions Bot deleted a comment from AppVeyorBot Oct 14, 2026

@jimklimov jimklimov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update!

@github-actions github-actions Bot deleted a comment from AppVeyorBot Oct 15, 2026
Signed-off-by: Jim Klimov <jimklimov+nut@gmail.com>
@github-actions github-actions Bot deleted a comment from AppVeyorBot Oct 26, 2026
@github-actions github-actions Bot deleted a comment from AppVeyorBot Oct 26, 2026
@AppVeyorBot

Copy link
Copy Markdown

✅ Build nut 2.8.5.5914-master completed (commit c05c0e84e4 by @jimklimov)

@AppVeyorBot

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI For good or bad, machine tools are upon us. Humans are still the responsible ones. enhancement Incorrect or missing readings On some devices driver-reported values are systemically off (e.g. x10, x0.1, const+Value, etc.) Ragtech Devices made by Ragtech Sistemas de Energia Shutdowns and overrides and battery level triggers Issues and PRs about system shutdown, especially if battery charge/runtime remaining is involved

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants