Skip to content

Extend BatteryMeter display - #1967

Open
BenBE wants to merge 11 commits into
htop-dev:mainfrom
BenBE:battery-extension
Open

Extend BatteryMeter display#1967
BenBE wants to merge 11 commits into
htop-dev:mainfrom
BenBE:battery-extension

Conversation

@BenBE

@BenBE BenBE commented Apr 19, 2026

Copy link
Copy Markdown
Member

This is the beginning of a series of commits to extend the BatteryMeter to provide additional information like battery capacity, charge rate and (dis)charge time estimates.

For the BSD parts of this PR I'll need some input from the people on these platforms regarding the actual interface to use to get the following values:

  • current power draw (voltage + current OR direct)
  • battery capacity and current charge
  • time to (dis)charge (can potentially be filled in generically if the power draw is available)

Pointers for sample code on each of the different platforms would be nice.

@BenBE BenBE added enhancement Extension or improvement to existing feature Linux 🐧 Linux related issues FreeBSD 👹 FreeBSD related issues MacOS 🍏 MacOS / Darwin related issues BSD 🐡 Issues related to *BSD PCP PCP related issues Solaris Solaris, Illumos, OmniOS, OpenIndiana NetBSD 🎏 NetBSD related issues OpenBSD 🐡 OpenBSD related issues DragonflyBSD 🪰 DragonflyBSD related issues labels Apr 19, 2026
@christianhorn

Copy link
Copy Markdown

.oO(Nice, quite a stunt, to cover all of the BSD's with this. :)

@BenBE
BenBE force-pushed the battery-extension branch from 1dc77c5 to 4d3bece Compare April 23, 2026 05:17
@BenBE
BenBE force-pushed the battery-extension branch 2 times, most recently from 98680b3 to 422d5f3 Compare May 1, 2026 08:38
@BenBE
BenBE force-pushed the battery-extension branch from 422d5f3 to dca06ec Compare May 15, 2026 13:19
@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a unified BatteryInfo structure for AC status, percentage, power, and energy values. BatteryMeter classifies battery state, calculates optional time estimates, and renders expanded or compact output. Platform collectors now populate BatteryInfo through native battery APIs, sysfs, procfs, ACPI, IOKit, sensors, or PCP metrics. PCP adds Denki battery metric identifiers and mappings.

Suggested reviewers: germanaizek

Poem

One struct gathers charge and power,
Across each platform, hour by hour.
AC states and energy align,
Displays report the changing line.
Battery data now shares one design.

Mergeability Score: 🟡 Moderate · up to 55a56

The PR can report incorrect or missing battery percentage, power, charging state, and time estimates on Linux systems, including systems with multiple power supplies or limited battery attributes. Merge should wait until these bounded reporting defects are fixed or explicitly accepted by the owner.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@BenBE
BenBE marked this pull request as ready for review May 15, 2026 18:04
@BenBE
BenBE force-pushed the battery-extension branch from dca06ec to 853701f Compare May 15, 2026 18:04
@BenBE BenBE added this to the 3.6.0 milestone May 15, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
netbsd/Platform.c (1)

471-485: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Release the proplib objects on every exit path.

prop_dictionary_recv_ioctl() returns a dictionary owned by the caller, prop_dictionary_iterator() and prop_array_iterator() create iterator objects that must be released explicitly, and iterators retain their underlying collections. Currently, dict, devIter, and each fieldsIter are never released, causing a memory leak on every call to this function. With repeated battery polling, this accumulates over time.

All three objects must be released:

  • dict via prop_object_release() (owned by caller from prop_dictionary_recv_ioctl())
  • devIter via prop_object_iterator_release() (created by prop_dictionary_iterator())
  • fieldsIter via prop_object_iterator_release() (created by prop_array_iterator())
Possible fix
 void Platform_getBattery(BatteryInfo* info) {
-   prop_dictionary_t dict, fields, props;
+   prop_dictionary_t dict = NULL, fields, props;
    prop_object_t device, class;
+   prop_object_iterator_t devIter = NULL;
+   prop_object_iterator_t fieldsIter = NULL;
@@
-   prop_object_iterator_t devIter = prop_dictionary_iterator(dict);
+   devIter = prop_dictionary_iterator(dict);
    if (devIter == NULL)
       goto error;
@@
-      prop_object_iterator_t fieldsIter = prop_array_iterator(fieldsArray);
+      fieldsIter = prop_array_iterator(fieldsArray);
       if (fieldsIter == NULL)
          goto error;
@@
       while ((fields = prop_object_iterator_next(fieldsIter)) != NULL) {
          ...
       }
+
+      prop_object_iterator_release(fieldsIter);
+      fieldsIter = NULL;
    }
+
+   prop_object_iterator_release(devIter);
+   devIter = NULL;
+   prop_object_release(dict);
+   dict = NULL;
 
 error:
+   if (fieldsIter != NULL)
+      prop_object_iterator_release(fieldsIter);
+   if (devIter != NULL)
+      prop_object_iterator_release(devIter);
+   if (dict != NULL)
+      prop_object_release(dict);
    if (fd != -1)
       close(fd);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@netbsd/Platform.c` around lines 471 - 485, The code leaks proplib objects:
the dictionary returned by prop_dictionary_recv_ioctl (dict) and the iterators
created by prop_dictionary_iterator (devIter) and prop_array_iterator
(fieldsIter) must be released on every exit path; update the function so that
before jumping to the error/exit label or returning you call
prop_object_release(dict) when dict is non-NULL and
prop_object_iterator_release(devIter) and
prop_object_iterator_release(fieldsIter) when those iterators are non-NULL (also
release any fieldsIter created inside the loop before continuing), ensuring you
don't release objects twice and that fieldsArray/device handling remains
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@BatteryMeter.c`:
- Around line 116-141: The compact-mode token concatenation happens because the
AC/bat labels written by xSnprintf (the calls that print "%s" with "AC"/"AC+bat"
and the "bat" literal) lack a trailing separator; change those format strings to
include a space (e.g., "%s " and "bat ") so tokens don't get glued, and when
printing power in the xSnprintf call that uses info.powerCurr (inside the
isCharging || isDischarging branch) normalize the sign by printing the absolute
value (use fabs(info.powerCurr) or equivalent) so discharging shows positive
watts consistent with text mode; update the xSnprintf invocations that append to
buf/len accordingly.

In `@darwin/Platform.c`:
- Around line 734-761: The code is incorrectly assigning raw percentage values
(cap_current/cap_max) into Wh fields info->energyCurr and info->energyFull;
remove the two assignments so only info->percent = 100.0 * cap_current / cap_max
is kept and leave info->energyCurr and info->energyFull as NaN (do not populate
them from cap_current/cap_max). Update the block that checks cap_max > 0.0 (the
lines that set info->energyCurr = cap_current; and info->energyFull = cap_max;)
to remove those assignments and keep only the percent calculation.

In `@linux/Platform.c`:
- Around line 1095-1104: Reverse the probe order so sysfs is tried before
procfs: when Platform_Battery_method is BAT_SYS call
Platform_Battery_getSysData(&Platform_Battery_cache) first and if
isNonnegative(Platform_Battery_cache.percent) leave method as BAT_SYS; if that
fails set Platform_Battery_method = BAT_PROC and call
Platform_Battery_getProcData(&Platform_Battery_cache) as a fallback and only
then set Platform_Battery_method = BAT_ERR if percent is still not nonnegative.
Use the existing symbols Platform_Battery_method, Platform_Battery_getSysData,
Platform_Battery_getProcData, Platform_Battery_cache, BAT_SYS, BAT_PROC, BAT_ERR
and isNonnegative to implement this change.
- Around line 841-844: Platform_Battery_getProcData currently replaces a valid
procfs percent with NAN whenever procAcpiCheck() fails; change it to always read
the procfs battery percentage and only set percent to NAN if
Platform_Battery_getProcBatInfo() itself indicates failure. Concretely, call
Platform_Battery_getProcBatInfo() unconditionally to populate info->percent,
assign info->ac = procAcpiCheck() but leave info->ac as AC_ERROR if adapter
detection failed, and do not overwrite a valid percent with NAN based solely on
procAcpiCheck() failing; only set percent to NAN when
Platform_Battery_getProcBatInfo() reports an error.

In `@openbsd/Platform.c`:
- Around line 391-457: The code only calls findDevice("acpibat0", ...) which
collects battery metrics from a single pack; change the logic to iterate over
all acpibat devices (e.g., for i = 0; findDevice(name, mib, &snsrdev, &sdlen);
++i) using a formatted name like "acpibat%d" and accumulate totalFull,
totalRemain and totalPower per-device (the blocks that read SENSOR_WATTHOUR,
SENSOR_INTEGER, SENSOR_WATTS and update batteryFull, batteryRemain,
batteryState, batteryPower) into the existing totals; keep the final
percent/energy/power calculations using the aggregated totals (referencing
findDevice, totalFull, totalRemain, totalPower, and the sysctl queries for
SENSOR_WATTHOUR/SENSOR_WATTS/SENSOR_INTEGER).

In `@pcp/Platform.c`:
- Around line 880-883: The AC state logic is inverted: instead of setting
info->ac = AC_PRESENT when count < 1, set AC_PRESENT when there is at least one
battery (count >= 1) and then override to AC_ABSENT if any battery is
discharging (power < 0). Update the block that currently checks count and
assigns info->ac so the flow is: if count < 1 set a fallback/ERROR state (or
return appropriately), otherwise set info->ac = AC_PRESENT, iterate battery
instances to check power and if any power < 0 set info->ac = AC_ABSENT; apply
the same fix to the analogous block around the second occurrence (the block
referenced at lines ~912-914). Use the existing symbols info->ac, AC_PRESENT,
AC_ABSENT, count and the battery power checks to locate and change the logic.
- Around line 892-893: The code uses batteryEnergyFull[i].d directly as the
CLAMP upper bound which can be negative and cause info->energyCurr to go
negative; compute a non-negative full value first (e.g., double full =
isNonnegative(batteryEnergyFull[i].d) ? batteryEnergyFull[i].d : 0) and then use
CLAMP(batteryEnergyCurr[i].d, 0, full) to update info->energyCurr and add full
(not the raw batteryEnergyFull) to info->energyFull so both updates guard
against negative full-capacity samples (references: info->energyCurr,
info->energyFull, batteryEnergyCurr, batteryEnergyFull, CLAMP, isNonnegative).

---

Outside diff comments:
In `@netbsd/Platform.c`:
- Around line 471-485: The code leaks proplib objects: the dictionary returned
by prop_dictionary_recv_ioctl (dict) and the iterators created by
prop_dictionary_iterator (devIter) and prop_array_iterator (fieldsIter) must be
released on every exit path; update the function so that before jumping to the
error/exit label or returning you call prop_object_release(dict) when dict is
non-NULL and prop_object_iterator_release(devIter) and
prop_object_iterator_release(fieldsIter) when those iterators are non-NULL (also
release any fieldsIter created inside the loop before continuing), ensuring you
don't release objects twice and that fieldsArray/device handling remains
unchanged.
🪄 Autofix (Beta)

❌ Autofix failed (check again to retry)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 02679205-318c-40a3-9203-a042a4323268

📥 Commits

Reviewing files that changed from the base of the PR and between b7f9df9 and 853701f.

📒 Files selected for processing (21)
  • BatteryMeter.c
  • BatteryMeter.h
  • darwin/Platform.c
  • darwin/Platform.h
  • dragonflybsd/Platform.c
  • dragonflybsd/Platform.h
  • freebsd/Platform.c
  • freebsd/Platform.h
  • linux/Platform.c
  • linux/Platform.h
  • netbsd/Platform.c
  • netbsd/Platform.h
  • openbsd/Platform.c
  • openbsd/Platform.h
  • pcp/Metric.h
  • pcp/Platform.c
  • pcp/Platform.h
  • solaris/Platform.c
  • solaris/Platform.h
  • unsupported/Platform.c
  • unsupported/Platform.h

Comment thread BatteryMeter.c
Comment thread darwin/Platform.c
Comment thread linux/Platform.c
Comment thread linux/Platform.c
Comment on lines 1095 to 1104
if (Platform_Battery_method == BAT_PROC) {
Platform_Battery_getProcData(percent, isOnAC);
if (!isNonnegative(*percent))
Platform_Battery_getProcData(&Platform_Battery_cache);
if (!isNonnegative(Platform_Battery_cache.percent))
Platform_Battery_method = BAT_SYS;
}
if (Platform_Battery_method == BAT_SYS) {
Platform_Battery_getSysData(percent, isOnAC);
if (!isNonnegative(*percent))
Platform_Battery_getSysData(&Platform_Battery_cache);
if (!isNonnegative(Platform_Battery_cache.percent))
Platform_Battery_method = BAT_ERR;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prefer sysfs before procfs for the new battery telemetry.

Platform_Battery_getProcData() never fills powerCurr or energy*, so the current BAT_PROC-first flow leaves the new rate/capacity/time-estimate inputs permanently NAN on hosts that expose both procfs and sysfs. Sysfs needs to be the preferred source now, with procfs kept as a fallback for older setups.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@linux/Platform.c` around lines 1095 - 1104, Reverse the probe order so sysfs
is tried before procfs: when Platform_Battery_method is BAT_SYS call
Platform_Battery_getSysData(&Platform_Battery_cache) first and if
isNonnegative(Platform_Battery_cache.percent) leave method as BAT_SYS; if that
fails set Platform_Battery_method = BAT_PROC and call
Platform_Battery_getProcData(&Platform_Battery_cache) as a fallback and only
then set Platform_Battery_method = BAT_ERR if percent is still not nonnegative.
Use the existing symbols Platform_Battery_method, Platform_Battery_getSysData,
Platform_Battery_getProcData, Platform_Battery_cache, BAT_SYS, BAT_PROC, BAT_ERR
and isNonnegative to implement this change.

Comment thread openbsd/Platform.c
Comment on lines 391 to 457
bool found = findDevice("acpibat0", mib, &snsrdev, &sdlen);

*percent = NAN;
if (found) {
bool haveTotalFull = false;
bool haveTotalRemain = false;
bool haveTotalPower = false;

int64_t totalFull = 0;
int64_t totalRemain = 0;
int64_t totalPower = 0;

/* See "sys/dev/acpi/acpibat.c" of OpenBSD source code for the indices
of the last field. */
mib[3] = SENSOR_WATTHOUR;
mib[4] = 0; /* "last full capacity" */
double last_full_capacity = 0;
bool haveBatteryFull = false;
int64_t batteryFull = 0;
if (sysctl(mib, 5, &s, &slen, NULL, 0) != -1)
last_full_capacity = s.value;
if (last_full_capacity > 0) {
batteryFull = s.value;

if (batteryFull > 0)
haveBatteryFull = true;

if (haveBatteryFull) {
mib[3] = SENSOR_WATTHOUR;
mib[4] = 3; /* "remaining capacity" */
if (sysctl(mib, 5, &s, &slen, NULL, 0) != -1) {
double charge = s.value;
*percent = 100 * (charge / last_full_capacity);
if (charge >= last_full_capacity) {
*percent = 100;
int64_t batteryRemain = s.value;
if (batteryRemain >= 0) {
totalRemain += batteryRemain;
totalFull += batteryFull;
haveTotalRemain = true;
haveTotalFull = true;
}
}
}

if (haveTotalRemain && haveTotalFull && totalFull > 0) {
info->percent = ((double) totalRemain * 100.0) / (double) totalFull;
if (totalRemain >= totalFull)
info->percent = 100;

info->energyCurr = (double) totalRemain / 1000000.0;
info->energyFull = (double) totalFull / 1000000.0;
}

mib[3] = SENSOR_INTEGER;
mib[4] = 0; /* "battery state" */
int64_t batteryState = 0;
if (sysctl(mib, 5, &s, &slen, NULL, 0) != -1)
batteryState = s.value;

mib[3] = SENSOR_WATTS;
mib[4] = 0; /* "rate" */
if (sysctl(mib, 5, &s, &slen, NULL, 0) != -1) {
int64_t batteryPower = s.value;
if (batteryState & 0x01)
batteryPower = -batteryPower;

totalPower += batteryPower;
haveTotalPower = true;
}

if (haveTotalPower) {
info->powerCurr = (double) totalPower / 1000000.0;
}
}

@coderabbitai coderabbitai Bot May 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't hardcode a single battery device.

Line 391 only queries acpibat0, so totalFull, totalRemain, and totalPower can never include any other battery device even though this code now derives aggregate battery totals from them. On multi-battery hardware, the reported percent, Wh, and time estimates will reflect only the first pack.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openbsd/Platform.c` around lines 391 - 457, The code only calls
findDevice("acpibat0", ...) which collects battery metrics from a single pack;
change the logic to iterate over all acpibat devices (e.g., for i = 0;
findDevice(name, mib, &snsrdev, &sdlen); ++i) using a formatted name like
"acpibat%d" and accumulate totalFull, totalRemain and totalPower per-device (the
blocks that read SENSOR_WATTHOUR, SENSOR_INTEGER, SENSOR_WATTS and update
batteryFull, batteryRemain, batteryState, batteryPower) into the existing
totals; keep the final percent/energy/power calculations using the aggregated
totals (referencing findDevice, totalFull, totalRemain, totalPower, and the
sysctl queries for SENSOR_WATTHOUR/SENSOR_WATTS/SENSOR_INTEGER).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Can you point to documentation where it describes how to best find the number of available devices and how they are named?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@BenBE — here are the relevant references:

Official documentation:

  • acpibat(4) — describes the driver; battery status is accessible via sysctl(8) or sensorsd(8).
  • sysctl(2) — documents the HW_SENSORS MIB layout: the third level is an array of struct sensordev structures for each sensor device, and the third/fourth/fifth levels together address individual struct sensor readings. The third level is the numeric device index whose xname is matched against the device name.

The enumeration protocol (already implemented in your findDevice()) is: iterate mib[2] from 0; ENXIO means no device at this slot (skip); ENOENT means no more devices (stop). Batteries are named acpibat0, acpibat1, etc.

Real-world confirmation:

A two-battery system exposes hw.sensors.acpibat0.* and hw.sensors.acpibat1.* as sibling sensordev entries with identical sensor indices.

Prior-art for the iteration pattern:

The distatus/battery library on OpenBSD does exactly this: it loops over consecutive MIB indices, calls sysctl for each sensordev, breaks on ENOENT, and checks whether xname has the prefix "acpibat".

Suggested adaptation for Platform_getBattery: instead of calling findDevice("acpibat0", ...) once, inline the sensordev scan and match any xname with strncmp(snsrdev.xname, "acpibat", 7) == 0 (or equivalent), accumulating totalFull/totalRemain/totalPower across all matching devices before computing the final percent/energy/power fields.

Comment thread pcp/Platform.c Outdated
Comment thread pcp/Platform.c Outdated
@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

An unexpected error occurred while generating fixes: Not Found - https://docs.github.com/rest/git/refs#get-a-reference

@BenBE
BenBE force-pushed the battery-extension branch from 853701f to 0e7353e Compare May 15, 2026 18:22

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@BatteryMeter.c`:
- Around line 83-88: The block that appends an extra separator (the if (ret &&
len > 2) { *buf++ = ','; *buf++ = ' '; *buf = 0; len -= 2; }) causes a double
comma in TEXT_METERMODE output because the AC prefix written earlier already
includes ", ". Remove this entire conditional block (references: variables ret,
len, buf and the TEXT_METERMODE output path) so no additional ", " is appended;
no other changes are needed to the surrounding code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5eef6dbc-1164-41ed-9430-663fb5dacd29

📥 Commits

Reviewing files that changed from the base of the PR and between 853701f and 0e7353e.

📒 Files selected for processing (1)
  • BatteryMeter.c

Comment thread BatteryMeter.c
@BenBE
BenBE force-pushed the battery-extension branch from 0e7353e to debe731 Compare May 15, 2026 21:45

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
BatteryMeter.c (1)

137-141: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize discharging watts sign in compact mode.

Line 140 prints raw info.powerCurr, so discharging displays negative watts, while text mode (Line 93) shows positive watts for discharging. Keep both modes consistent.

Suggested patch
       if (isCharging || isDischarging) {
          ret = xSnprintf(
             buf, len, "%.1fW @ %.1f/%.1fWh",
-            info.powerCurr, info.energyCurr, info.energyFull
+            isDischarging ? -info.powerCurr : info.powerCurr,
+            info.energyCurr, info.energyFull
          );
          buf += ret; len -= ret;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BatteryMeter.c` around lines 137 - 141, Compact-mode watt value uses raw
info.powerCurr causing negative watts during discharging; update the xSnprintf
call inside the if (isCharging || isDischarging) block to pass a normalized
(absolute) watt value so discharging shows positive watts like text mode — e.g.
replace the first format argument info.powerCurr with a normalized expression
(use -info.powerCurr when isDischarging, or fabsf(info.powerCurr)) while keeping
the rest of the arguments (info.energyCurr, info.energyFull) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@BatteryMeter.c`:
- Around line 49-51: The percent-only fallback in BatteryMeter.c (when
haveEnergy is false) currently writes only "%.1f%%" to this->txtBuffer and
returns, losing AC/battery context; modify the early-return branch where
haveEnergy is checked to include info.ac in the formatted string (use info.ac to
choose an "AC"/"Battery" marker or a suffix/prefix) so that the call that writes
to this->txtBuffer includes both the percent (info.percent) and the AC state
(info.ac) before returning.

---

Duplicate comments:
In `@BatteryMeter.c`:
- Around line 137-141: Compact-mode watt value uses raw info.powerCurr causing
negative watts during discharging; update the xSnprintf call inside the if
(isCharging || isDischarging) block to pass a normalized (absolute) watt value
so discharging shows positive watts like text mode — e.g. replace the first
format argument info.powerCurr with a normalized expression (use -info.powerCurr
when isDischarging, or fabsf(info.powerCurr)) while keeping the rest of the
arguments (info.energyCurr, info.energyFull) unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ac465c8f-a119-478f-8226-175dae17a47b

📥 Commits

Reviewing files that changed from the base of the PR and between 0e7353e and debe731.

📒 Files selected for processing (1)
  • BatteryMeter.c

Comment thread BatteryMeter.c
@BenBE

BenBE commented May 15, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai:

BatteryMeter.c (1)> 137-141: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize discharging watts sign in compact mode.
Line 140 prints raw info.powerCurr, so discharging displays negative watts, while text mode (Line 93) shows positive watts for discharging. Keep both modes consistent.

Suggested patch

       if (isCharging || isDischarging) {
          ret = xSnprintf(
             buf, len, "%.1fW @ %.1f/%.1fWh",
-            info.powerCurr, info.energyCurr, info.energyFull
+            isDischarging ? -info.powerCurr : info.powerCurr,
+            info.energyCurr, info.energyFull
          );
          buf += ret; len -= ret;

In compact mode, better to include the sign (negative for discharging, positive for charging).

@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@BenBE: Understood — preserving the sign in compact mode is a deliberate design choice, where negative signals discharge direction and positive signals charge, avoiding the need for extra labels. That's a reasonable convention given the space constraints of compact mode. No change needed here.

@BenBE
BenBE force-pushed the battery-extension branch from debe731 to fb0534a Compare May 16, 2026 16:43

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
netbsd/Platform.c (1)

471-485: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Release proplib objects to prevent memory leak.

dict, devIter, and each fieldsIter must be released via prop_object_release() when no longer needed. NetBSD proplib objects are reference-counted and require explicit release—failure to do so leaks memory on every battery refresh cycle in long-running sessions. Release on both success and error paths.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0a0ac52a-cfb8-45fa-b14d-6af829fe988e

📥 Commits

Reviewing files that changed from the base of the PR and between debe731 and fb0534a.

📒 Files selected for processing (12)
  • BatteryMeter.c
  • BatteryMeter.h
  • darwin/Platform.c
  • dragonflybsd/Platform.c
  • freebsd/Platform.c
  • linux/Platform.c
  • netbsd/Platform.c
  • openbsd/Platform.c
  • pcp/Metric.h
  • pcp/Platform.c
  • solaris/Platform.c
  • unsupported/Platform.c

Comment thread darwin/Platform.c
Comment thread dragonflybsd/Platform.c
Comment thread freebsd/Platform.c
Comment thread linux/Platform.c
Comment thread linux/Platform.c
@BenBE
BenBE force-pushed the battery-extension branch from fb0534a to 58662f6 Compare May 16, 2026 16:58

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
netbsd/Platform.c (1)

471-486: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Resource leak: proplib objects never released.

dict, devIter, and fieldsIter are allocated but never freed. The fieldsIter is created each loop iteration without release. Per proplib API, these require prop_object_release() and prop_object_iterator_release().

Proposed fix
 void Platform_getBattery(BatteryInfo* info) {
-   prop_dictionary_t dict, fields, props;
+   prop_dictionary_t dict = NULL;
+   prop_dictionary_t fields, props;
    prop_object_t device, class;
+   prop_object_iterator_t devIter = NULL;

    // ... initialization unchanged ...

    if (prop_dictionary_recv_ioctl(fd, ENVSYS_GETDICTIONARY, &dict) != 0)
-      goto error;
+      goto cleanup;

-   prop_object_iterator_t devIter = prop_dictionary_iterator(dict);
+   devIter = prop_dictionary_iterator(dict);
    if (devIter == NULL)
-      goto error;
+      goto cleanup;

    while ((device = prop_object_iterator_next(devIter)) != NULL) {
       prop_object_t fieldsArray = prop_dictionary_get_keysym(dict, device);
       if (fieldsArray == NULL)
-         goto error;
+         goto cleanup;

       prop_object_iterator_t fieldsIter = prop_array_iterator(fieldsArray);
       if (fieldsIter == NULL)
-         goto error;
+         goto cleanup;

       // ... process fields ...

+      prop_object_iterator_release(fieldsIter);
    }

-error:
+cleanup:
+   if (devIter)
+      prop_object_iterator_release(devIter);
+   if (dict)
+      prop_object_release(dict);
    if (fd != -1)
       close(fd);
 }
♻️ Duplicate comments (2)
dragonflybsd/Platform.c (1)

479-486: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prefer sysctl percent as authoritative when aggregation is incomplete.

The aggregation loop may skip battery units on ioctl failure or missing voltage data. Overwriting info->percent with the partial aggregate can produce an incorrect percentage compared to the kernel-reported hw.acpi.battery.life value already stored at line 382. Only set info->percent from the aggregate when the sysctl failed (i.e., info->percent is still NAN).

Proposed fix
    if (haveTotalRemain && haveTotalFull && totalFull > 0) {
-      info->percent = ((double) totalRemain * 100.0) / (double) totalFull;
-      if (totalRemain >= totalFull)
-         info->percent = 100;
+      if (isnan(info->percent)) {
+         info->percent = ((double) totalRemain * 100.0) / (double) totalFull;
+         if (totalRemain >= totalFull)
+            info->percent = 100.0;
+      }
 
       info->energyCurr = (double) totalRemain / 1000000.0;
       info->energyFull = (double) totalFull / 1000000.0;
freebsd/Platform.c (1)

507-514: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep sysctl percent as fallback for incomplete aggregation.

Same issue as DragonFlyBSD: the loop can skip units when ACPIIO_BATT_GET_BIX or ACPIIO_BATT_GET_BST fails. The kernel-reported hw.acpi.battery.life (already stored at line 415) is more reliable than a partial aggregate. Only overwrite info->percent when the sysctl value is unavailable.

Proposed fix
    if (haveTotalRemain && haveTotalFull && totalFull > 0) {
-      info->percent = ((double) totalRemain * 100.0) / (double) totalFull;
-      if (totalRemain >= totalFull)
-         info->percent = 100;
+      if (isnan(info->percent)) {
+         info->percent = ((double) totalRemain * 100.0) / (double) totalFull;
+         if (totalRemain >= totalFull)
+            info->percent = 100.0;
+      }
 
       info->energyCurr = (double) totalRemain / 1000000.0;
       info->energyFull = (double) totalFull / 1000000.0;

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0881be70-ec0a-435f-9e26-53a032962ece

📥 Commits

Reviewing files that changed from the base of the PR and between fb0534a and 58662f6.

📒 Files selected for processing (12)
  • BatteryMeter.c
  • BatteryMeter.h
  • darwin/Platform.c
  • dragonflybsd/Platform.c
  • freebsd/Platform.c
  • linux/Platform.c
  • netbsd/Platform.c
  • openbsd/Platform.c
  • pcp/Metric.h
  • pcp/Platform.c
  • solaris/Platform.c
  • unsupported/Platform.c

Comment thread BatteryMeter.c
Comment thread BatteryMeter.c Outdated
Comment thread BatteryMeter.c
Comment thread BatteryMeter.h Outdated
ACPresence ac;

double percent; /* [0..100], NAN if unknown */
double powerCurr; /* instantaneous power in W, NAN if unknown */

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Document the powerCurr sign contract.

BatteryMeter_updateValues() derives charging vs. discharging entirely from this field's sign, but the header only documents the unit. Please state the convention here explicitly, e.g. positive while charging and negative while discharging, so platform collectors implement the same contract.

Comment thread linux/Platform.c
Comment on lines +1033 to +1044
if (!haveBatteryPower && haveBatteryCurrent && haveBatteryVoltage) {
batteryPower = (batteryCurrent * batteryVoltage) / 1000000;
haveBatteryPower = true;
}

if (!now && full && isNonnegative(capacityLevel))
totalRemain += capacityLevel * fullCharge;
if (haveBatteryPower) {
if (batteryIsDischarging)
batteryPower = -batteryPower;

totalPower += batteryPower;
havePower = true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check Linux kernel documentation and driver conventions for CURRENT_NOW signedness
rg -n -C3 "CURRENT_NOW" --type=c | head -80

# Search for any existing handling of signed current in power_supply drivers
ast-grep --pattern 'POWER_SUPPLY_PROP_CURRENT_NOW'

Repository: htop-dev/htop

Length of output: 399


🏁 Script executed:

#!/bin/bash
# Get the full context of the battery power calculation
echo "=== Checking variable declarations and initialization ==="
rg -n "batteryCurrent|batteryPower|batteryIsDischarging|batteryVoltage" linux/Platform.c | head -40

echo -e "\n=== Full battery reading section ==="
sed -n '950,1050p' linux/Platform.c

Repository: htop-dev/htop

Length of output: 4456


🏁 Script executed:

#!/bin/bash
# Check how val is parsed - does it handle negative numbers?
echo "=== Checking val parsing context ==="
sed -n '900,950p' linux/Platform.c

# Look for any comments about battery or current in the file
echo -e "\n=== Comments about battery/power handling ==="
rg -n "battery|CURRENT|current" linux/Platform.c | grep -i "comment\|negative\|discharge" | head -20

Repository: htop-dev/htop

Length of output: 1742


🌐 Web query:

Linux kernel POWER_SUPPLY_PROP_CURRENT_NOW sysfs convention signedness negative discharge

💡 Result:

In the Linux kernel power supply class, the sysfs attribute POWER_SUPPLY_PROP_CURRENT_NOW follows a specific convention regarding signedness to distinguish between charging and discharging states [1][2]. According to the official kernel documentation, the convention is as follows: 1. Signedness: The value is treated as a signed integer [1][2]. 2. Negative Values: Negative values are used to represent battery discharging (i.e., current flowing out of the battery to power a load) [1][2]. 3. Positive Values: Positive values are used to represent battery charging (i.e., current flowing into the battery) [1][2]. This convention allows user-space applications to determine the direction of current flow for battery-backed devices by checking the sign of the integer reported in the current_now sysfs file [1][2]. For other types of power supplies, such as USB IBUS, positive values are typically used to report the current being supplied [1][2]. Drivers are responsible for converting their raw hardware readings into these standardized units (microamperes) and applying the correct sign based on the operational state [3][4].

Citations:


🏁 Script executed:

#!/bin/bash
# Check how totalPower is used and any sign convention documentation
echo "=== Usage of totalPower after calculation ==="
sed -n '1040,1100p' linux/Platform.c

echo -e "\n=== Look for any function comments about sign convention ==="
sed -n '850,920p' linux/Platform.c | grep -A5 "^[[:space:]]*//.*power\|^[[:space:]]*/*.*power"

# Check if there's any test or usage that shows expected sign convention
echo -e "\n=== Search for power-related struct/API that shows sign expectation ==="
rg -n "totalPower|batteryPower" linux/Platform.c | tail -20

Repository: htop-dev/htop

Length of output: 2333


Remove the STATUS-based negation; CURRENT_NOW is already signed per Linux kernel convention.

The Linux power supply API reports CURRENT_NOW as a signed value where negative indicates discharge and positive indicates charge. Since batteryCurrent is read directly from CURRENT_NOW (line 1034), the power calculation already has the correct sign. The subsequent negation at line 1040 based on batteryIsDischarging inverts this, causing discharge to report positive power.

Remove the sign negation or check whether batteryCurrent is already negative before applying the STATUS-based correction.

Comment thread pcp/Platform.c Outdated
@BenBE
BenBE force-pushed the battery-extension branch 2 times, most recently from 368e9fb to 876564f Compare June 3, 2026 08:20

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
netbsd/Platform.c (1)

471-476: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Proplib iterator not released before return.

devIter is created at line 474 but never released. When jumping to error: or reaching the end of iteration, prop_object_iterator_release(devIter) should be called. Similarly for fieldsIter created at line 483.

Proposed fix
+   prop_object_iterator_t devIter = NULL;
+   prop_object_iterator_t fieldsIter = NULL;
+
    int fd = open(_PATH_SYSMON, O_RDONLY);
    if (fd == -1)
       goto error;

    if (prop_dictionary_recv_ioctl(fd, ENVSYS_GETDICTIONARY, &dict) != 0)
       goto error;

-   prop_object_iterator_t devIter = prop_dictionary_iterator(dict);
+   devIter = prop_dictionary_iterator(dict);
    if (devIter == NULL)
       goto error;

    while ((device = prop_object_iterator_next(devIter)) != NULL) {
       // ...
-      prop_object_iterator_t fieldsIter = prop_array_iterator(fieldsArray);
+      fieldsIter = prop_array_iterator(fieldsArray);
       if (fieldsIter == NULL)
          goto error;
       // ... loop body ...
+      prop_object_iterator_release(fieldsIter);
+      fieldsIter = NULL;
    }
+   prop_object_iterator_release(devIter);
+   devIter = NULL;
    // ... percent/power calculations ...

 error:
+   if (fieldsIter)
+      prop_object_iterator_release(fieldsIter);
+   if (devIter)
+      prop_object_iterator_release(devIter);
+   if (dict)
+      prop_object_release(dict);
    if (fd != -1)
       close(fd);
 }

Also applies to: 637-640

♻️ Duplicate comments (2)
pcp/Platform.c (1)

891-894: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard energyCurr from invalid full-capacity samples.

Line 892 uses batteryEnergyFull[i].d directly as the CLAMP upper bound. If that value is negative, energyCurr accumulates an incorrect contribution while line 893 correctly excludes it from energyFull. Compute the sanitized full value once and use it consistently:

Proposed fix
       info->energyCurr = 0.0;
       info->energyFull = 0.0;
       for (i = 0; i < count; i++) {
-         info->energyCurr += CLAMP(batteryEnergyCurr[i].d, 0, batteryEnergyFull[i].d);
-         info->energyFull += isNonnegative(batteryEnergyFull[i].d) ? batteryEnergyFull[i].d : 0;
+         double full = isNonnegative(batteryEnergyFull[i].d) ? batteryEnergyFull[i].d : 0.0;
+         info->energyCurr += CLAMP(batteryEnergyCurr[i].d, 0.0, full);
+         info->energyFull += full;
       }
linux/Platform.c (1)

841-844: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Procfs percent discarded when AC detection fails.

Line 843 sets info->percent = NAN when procAcpiCheck() returns AC_ERROR, even though /proc/acpi/battery/* may still contain valid capacity data. The AC adapter state and battery capacity are independent data sources.

Proposed fix
 static void Platform_Battery_getProcData(BatteryInfo* info) {
    info->ac = procAcpiCheck();
-   info->percent = AC_ERROR != info->ac ? Platform_Battery_getProcBatInfo() : NAN;
+   info->percent = Platform_Battery_getProcBatInfo();
 }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c9b26473-03af-404a-85f4-d4af978bc086

📥 Commits

Reviewing files that changed from the base of the PR and between fb0534a and 876564f.

📒 Files selected for processing (21)
  • BatteryMeter.c
  • BatteryMeter.h
  • darwin/Platform.c
  • darwin/Platform.h
  • dragonflybsd/Platform.c
  • dragonflybsd/Platform.h
  • freebsd/Platform.c
  • freebsd/Platform.h
  • linux/Platform.c
  • linux/Platform.h
  • netbsd/Platform.c
  • netbsd/Platform.h
  • openbsd/Platform.c
  • openbsd/Platform.h
  • pcp/Metric.h
  • pcp/Platform.c
  • pcp/Platform.h
  • solaris/Platform.c
  • solaris/Platform.h
  • unsupported/Platform.c
  • unsupported/Platform.h

@BenBE
BenBE force-pushed the battery-extension branch from 876564f to d9be204 Compare July 19, 2026 19:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (7)
BatteryMeter.h (1)

21-29: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Document the powerCurr sign convention.

Still undocumented: only the unit is specified, not the sign contract. BatteryMeter_updateValues() derives charging/discharging purely from powerCurr's sign (negative = discharging, positive = charging per BatteryMeter.c logic), so every platform collector must implement this convention identically. State it explicitly in the comment.

Suggested doc fix
-   double powerCurr;        /* instantaneous power in W, NAN if unknown */
+   double powerCurr;        /* instantaneous power in W, NAN if unknown;
+                                positive while charging, negative while discharging */
BatteryMeter.c (3)

38-44: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clamp percent to the documented [0..100] range.

isNonnegative only rejects negative/NAN values; anything above 100 flows straight into this->values[0] even though the meter's total is fixed at 100 and the header documents [0..100].

Suggested fix
    this->values[0] = info.percent;
+   this->values[0] = CLAMP(this->values[0], 0.0, 100.0);

46-54: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Percent-only fallback drops AC state and available power telemetry.

When !haveEnergy, the function returns showing only %.1f%%, discarding both info.ac context and any usable info.powerCurr (per the stack context, Darwin can supply power without full/current energy). This makes the new charge/discharge-rate work invisible on platforms that only expose power, and hides AC/battery context users previously had.

Suggested fix
-   if (!haveEnergy) {
-      xSnprintf(this->txtBuffer, sizeof(this->txtBuffer), "%.1f%%", info.percent);
-      return;
-   }
+   bool havePowerOnly = !haveEnergy && isfinite(info.powerCurr);
+   if (!haveEnergy) {
+      const char* src =
+         (info.ac == AC_PRESENT) ? " (AC)" :
+         (info.ac == AC_ABSENT)  ? " (bat)" : "";
+      if (havePowerOnly)
+         xSnprintf(this->txtBuffer, sizeof(this->txtBuffer), "%.1f%% (%+.1fW)%s", info.percent, info.powerCurr, src);
+      else
+         xSnprintf(this->txtBuffer, sizeof(this->txtBuffer), "%.1f%%%s", info.percent, src);
+      return;
+   }

61-68: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Charging ETA still targets 95%, but the label says "time to full" (also applies to 107-113).

timeMinutes for charging is computed against 0.95 * info.energyFull, while the corresponding display at lines 107-113 reads "time to full". A prior review flagged this exact mismatch and it's marked as addressed in an earlier commit, but the code here still reproduces the same pattern — please confirm whether this reflects an intentional design (e.g., treating 95% as practically "full" due to trickle-charge tail) that simply needs a comment, or whether the fix regressed.

Suggested fix (if unintentional)
-   } else if (isCharging && 0.95 * info.energyFull > info.energyCurr) {
-      /* ceil for charge */
-      timeMinutes = (int)ceil((0.95 * info.energyFull - info.energyCurr) / info.powerCurr * 60.0);
+   } else if (isCharging && info.energyFull > info.energyCurr) {
+      /* ceil for charge */
+      timeMinutes = (int)ceil((info.energyFull - info.energyCurr) / info.powerCurr * 60.0);
dragonflybsd/Platform.c (1)

479-486: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don't overwrite the system percentage with a partial energy aggregate.

info->percent was already set from hw.acpi.battery.life (line 382), a whole-system value. This block recomputes it from totalRemain/totalFull, which the loop populates only for units that pass every ioctl/voltage guard. On a multi-battery host where a subset fails, a valid system percentage gets replaced by a partial one. Guard the assignment on isnan(info->percent); keep the energy assignments unconditional.

Proposed fix
    if (haveTotalRemain && haveTotalFull && totalFull > 0) {
-      info->percent = ((double) totalRemain * 100.0) / (double) totalFull;
-      if (totalRemain >= totalFull)
-         info->percent = 100;
+      if (isnan(info->percent)) {
+         info->percent = ((double) totalRemain * 100.0) / (double) totalFull;
+         if (totalRemain >= totalFull)
+            info->percent = 100.0;
+      }

       info->energyCurr = (double) totalRemain / 1000000.0;
       info->energyFull = (double) totalFull / 1000000.0;
    }
pcp/Platform.c (2)

876-877: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Partial Denki telemetry still dropped.

Unchanged from the prior review: this early return discards info->powerCurr whenever denki.bat.energy_now is unavailable, even if denki.bat.power_now exists. The BatteryInfo contract tolerates unknown energy fields, so a Denki install exposing only power should still surface it.


892-893: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

energyCurr clamp still uses unguarded batteryEnergyFull[i].d.

Unchanged from the prior review: a negative batteryEnergyFull[i].d sample lets CLAMP(batteryEnergyCurr[i].d, 0, batteryEnergyFull[i].d) go negative. Clamp against a non-negative full first, as previously proposed.

🐛 Proposed fix
       info->energyCurr = 0.0;
       info->energyFull = 0.0;
       for (i = 0; i < count; i++) {
-         info->energyCurr += CLAMP(batteryEnergyCurr[i].d, 0, batteryEnergyFull[i].d);
-         info->energyFull += isNonnegative(batteryEnergyFull[i].d) ? batteryEnergyFull[i].d : 0;
+         double full = isNonnegative(batteryEnergyFull[i].d) ? batteryEnergyFull[i].d : 0.0;
+         info->energyCurr += CLAMP(batteryEnergyCurr[i].d, 0.0, full);
+         info->energyFull += full;
       }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 07de2fe8-b13a-4184-af4d-b61f5adbd424

📥 Commits

Reviewing files that changed from the base of the PR and between 876564f and d9be204.

📒 Files selected for processing (21)
  • BatteryMeter.c
  • BatteryMeter.h
  • darwin/Platform.c
  • darwin/Platform.h
  • dragonflybsd/Platform.c
  • dragonflybsd/Platform.h
  • freebsd/Platform.c
  • freebsd/Platform.h
  • linux/Platform.c
  • linux/Platform.h
  • netbsd/Platform.c
  • netbsd/Platform.h
  • openbsd/Platform.c
  • openbsd/Platform.h
  • pcp/Metric.h
  • pcp/Platform.c
  • pcp/Platform.h
  • solaris/Platform.c
  • solaris/Platform.h
  • unsupported/Platform.c
  • unsupported/Platform.h

Comment thread BatteryMeter.c Outdated
Comment thread pcp/Platform.c Outdated
Comment on lines +896 to +898
if (info->energyFull > 0) {
info->percent = CLAMP((info->energyCurr / info->energyFull) * 100.0, 0.0, 100.0);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Omit braces around trivial single-statement if bodies.

Both blocks wrap a single trivial assignment; the styleguide requires omitting braces in this case.

As per coding guidelines: "Omit braces around simple single statements (return, break, continue, trivial assignments)."

♻️ Proposed fix
       if (info->energyFull > 0) {
-         info->percent = CLAMP((info->energyCurr / info->energyFull) * 100.0, 0.0, 100.0);
-      }
+         info->percent = CLAMP((info->energyCurr / info->energyFull) * 100.0, 0.0, 100.0);
@@
-   if (info->powerCurr < 0) {
-      info->ac = AC_ABSENT;
-   }
+   if (info->powerCurr < 0)
+      info->ac = AC_ABSENT;

Also applies to: 912-914

@BenBE
BenBE force-pushed the battery-extension branch from d9be204 to ae8492a Compare July 19, 2026 21:03

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 43d72b57-9ded-4c8a-bf00-53ae6fe836bb

📥 Commits

Reviewing files that changed from the base of the PR and between d9be204 and ae8492a.

📒 Files selected for processing (21)
  • BatteryMeter.c
  • BatteryMeter.h
  • darwin/Platform.c
  • darwin/Platform.h
  • dragonflybsd/Platform.c
  • dragonflybsd/Platform.h
  • freebsd/Platform.c
  • freebsd/Platform.h
  • linux/Platform.c
  • linux/Platform.h
  • netbsd/Platform.c
  • netbsd/Platform.h
  • openbsd/Platform.c
  • openbsd/Platform.h
  • pcp/Metric.h
  • pcp/Platform.c
  • pcp/Platform.h
  • solaris/Platform.c
  • solaris/Platform.h
  • unsupported/Platform.c
  • unsupported/Platform.h

Comment thread BatteryMeter.c
Comment thread BatteryMeter.c
Comment thread BatteryMeter.h
Comment thread dragonflybsd/Platform.c
Comment on lines 379 to +382
int life;
size_t life_len = sizeof(life);
if (sysctlbyname("hw.acpi.battery.life", &life, &life_len, NULL, 0) == -1)
*percent = NAN;
else
*percent = life;
if (sysctlbyname("hw.acpi.battery.life", &life, &life_len, NULL, 0) != -1)
info->percent = life;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does DragonFly BSD hw.acpi.battery.life sysctl return -1 when battery capacity is unknown?

💡 Result:

In DragonFly BSD, the sysctl hw.acpi.battery.life typically returns -1 when the battery capacity or status is unknown, unavailable, or when the battery is not detected [1]. The sysctl implementation in DragonFly BSD shares much of its codebase with FreeBSD's ACPI battery driver [2][3]. When the system's ACPI subsystem cannot retrieve a valid battery capacity—for example, if the battery is not plugged in, or the hardware fails to report information—it commonly initializes or resets these values to -1 to indicate an error or an unknown state [1]. Users monitoring battery status through system tools may observe this value when the kernel fails to get a successful reading from the ACPI battery interface [1][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant file and nearby consumers
git ls-files 'dragonflybsd/Platform.c' '*/Platform.c' '*/Machine.c' '*/Process*.c' '*/Battery*' '*/*.c' | sed -n '1,120p'

printf '\n--- dragonflybsd/Platform.c around the battery code ---\n'
sed -n '360,520p' dragonflybsd/Platform.c

printf '\n--- search for isNonnegative and percent fallback logic ---\n'
rg -n "isNonnegative|isnan\\(info->percent\\)|info->percent = life|battery.life|ACPIIO_BATT" -S .

Repository: htop-dev/htop

Length of output: 8797


Guard hw.acpi.battery.life against -1. sysctlbyname() can succeed with an unknown battery value, but -1 is not a usable percent here: it blocks the ACPI energy fallback and later shows up as N/A via isNonnegative(). Only assign values in the 0–100 range.

Comment thread pcp/Platform.c
Comment thread pcp/Platform.c
Comment on lines +891 to +909
if (energyCount > 0) {
pmAtomValue* batteryEnergyCurr = xCalloc(energyCount, sizeof(pmAtomValue));
pmAtomValue* batteryEnergyFull = xCalloc(energyCount, sizeof(pmAtomValue));
if (Metric_values(PCP_DENKI_ENERGY_NOW, batteryEnergyCurr, energyCount, PM_TYPE_DOUBLE) &&
Metric_values(PCP_DENKI_ENERGY_FULL, batteryEnergyFull, energyCount, PM_TYPE_DOUBLE)) {
info->energyCurr = 0.0;
info->energyFull = 0.0;
for (int i = 0; i < energyCount; i++) {
double full = isNonnegative(batteryEnergyFull[i].d) ? batteryEnergyFull[i].d : 0.0;
info->energyCurr += CLAMP(batteryEnergyCurr[i].d, 0.0, full);
info->energyFull += full;
}

if (info->energyFull > 0) {
info->percent = CLAMP((info->energyCurr / info->energyFull) * 100.0, 0.0, 100.0);
}
}
free(batteryEnergyCurr);
free(batteryEnergyFull);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant files and nearby context.
for f in pcp/Metric.h pcp/Platform.c; do
  echo "===== $f ====="
  wc -l "$f"
done

echo "===== pcp/Metric.h (around lines 100-120) ====="
sed -n '100,120p' pcp/Metric.h

echo "===== pcp/Platform.c (around mappings near lines 230-245) ====="
sed -n '230,245p' pcp/Platform.c

echo "===== pcp/Platform.c (around battery aggregation near lines 880-920) ====="
sed -n '880,920p' pcp/Platform.c

# Search for denki references and any documentation/comments about capacity semantics.
echo "===== grep denki ====="
rg -n "denki\.bat\.(power_now|energy_now|energy_full|capacity)|pmda-denki|fillstate|percent" -S .

Repository: htop-dev/htop

Length of output: 41367


🌐 Web query:

pmda-denki denki.bat.capacity fillstate percent energy_now documentation

💡 Result:

pmda-denki is a Performance Metrics Domain Agent (PMDA) for the Performance Co-Pilot (PCP) suite, designed to collect and expose electrical power consumption and battery metrics on Linux systems [1][2]. Regarding the specific battery metrics mentioned, the following details are documented: denki.bat.capacity This metric reports the battery charge level as a percentage [3]. denki.bat.energy_now This metric shows the current battery charge [3]. It is often used to represent the raw energy level (frequently in Watt-hours) read from the system filesystem [4]. Related Metrics While you mentioned "fillstate," the official documentation and handbook identify the following core metrics for battery management: - denki.bat.power_now: Represents the current consumption or charge rate of the battery, typically updated by firmware [3][4]. - denki.bat.energy_now_rate: This is a computed metric calculated by pmda-denki itself, representing the rate of charging or discharging in 60-second intervals [3][4]. pmda-denki is generally installed and managed via the PCP infrastructure (e.g., using the Install/Remove scripts located in $PCP_PMDAS_DIR/denki) [1][2]. It is designed to work with system-provided power data, such as Intel's RAPL (Running Average Power Limit) interface for CPU power and standard battery sysfs interfaces for battery data [1][5]. For further details, the authoritative documentation can be found in the pmda-denki handbook [3][6].

Citations:


denki.bat.capacity is a percentage, not an energy-capacity metric

pcp/Metric.h and pcp/Platform.c map PCP_DENKI_ENERGY_FULL to denki.bat.capacity, then pcp/Platform.c#L891-L909 treats it as Wh and derives energyFull/percent from mixed units. Rework this to use a real full-capacity metric, or leave energyFull/ETA unset when only fill-state is available.

📍 Affects 2 files
  • pcp/Platform.c#L891-L909 (this comment)
  • pcp/Metric.h#L110-L112
  • pcp/Platform.c#L237-L239

@BenBE
BenBE force-pushed the battery-extension branch from ae8492a to f8fe874 Compare July 19, 2026 21:37

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
freebsd/Platform.c (1)

495-498: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Overwrite clobbers the authoritative hw.acpi.battery.life percentage.

info->percent is set from hw.acpi.battery.life at Line 403 (overall battery state). Lines 496-498 unconditionally overwrite it with the aggregated subset, which is populated only when ACPIIO_BATT_GET_BIX/_BST succeed and voltage data is present. On multi-battery or partial-read systems the aggregate reflects only the reporting units, degrading a correct reading. The dragonflybsd sibling guards this overwrite with isnan(info->percent); freebsd is missing the same guard.

Proposed fix
    if (haveTotalRemain && haveTotalFull && totalFull > 0) {
-      info->percent = ((double) totalRemain * 100.0) / (double) totalFull;
-      if (totalRemain >= totalFull)
-         info->percent = 100;
+      if (isnan(info->percent)) {
+         info->percent = ((double) totalRemain * 100.0) / (double) totalFull;
+         if (totalRemain >= totalFull)
+            info->percent = 100;
+      }

       info->energyCurr = (double) totalRemain / 1000000.0;
       info->energyFull = (double) totalFull / 1000000.0;
    }
linux/Platform.c (1)

844-846: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don't discard a valid procfs percent when AC detection fails.

Line 846 sets info->percent = NAN whenever procAcpiCheck() returns AC_ERROR, causing Platform_getBattery() to abandon procfs even when /proc/acpi/battery/* still holds valid capacity data. Read the battery percentage unconditionally and leave ac = AC_ERROR only for the adapter state.

Suggested fix
 static void Platform_Battery_getProcData(BatteryInfo* info) {
    info->ac = procAcpiCheck();
-   info->percent = AC_ERROR != info->ac ? Platform_Battery_getProcBatInfo() : NAN;
+   info->percent = Platform_Battery_getProcBatInfo();
 }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 200d9016-31d0-41d3-82d5-28c60d392b9a

📥 Commits

Reviewing files that changed from the base of the PR and between ae8492a and f8fe874.

📒 Files selected for processing (21)
  • BatteryMeter.c
  • BatteryMeter.h
  • darwin/Platform.c
  • darwin/Platform.h
  • dragonflybsd/Platform.c
  • dragonflybsd/Platform.h
  • freebsd/Platform.c
  • freebsd/Platform.h
  • linux/Platform.c
  • linux/Platform.h
  • netbsd/Platform.c
  • netbsd/Platform.h
  • openbsd/Platform.c
  • openbsd/Platform.h
  • pcp/Metric.h
  • pcp/Platform.c
  • pcp/Platform.h
  • solaris/Platform.c
  • solaris/Platform.h
  • unsupported/Platform.c
  • unsupported/Platform.h

Comment thread linux/Platform.c
Comment on lines +1036 to +1038
if (!haveBatteryPower && haveBatteryCurrent && haveBatteryVoltage) {
batteryPower = (batteryCurrent * batteryVoltage) / 1000000;
haveBatteryPower = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Linux power_supply CURRENT_NOW sysfs negative value discharge sign convention

💡 Result:

According to the Linux kernel ABI documentation for the power supply class, the sign convention for current readings (such as current_now) is explicitly defined: negative values indicate that the battery is discharging, while positive values indicate that the battery is charging [1][2]. Specifically, for current_now, the documentation states that "negative values are used for discharging batteries, positive values for charging batteries and for USB IBUS current" [1]. While this is the established standard, it is important to note that historically, there has been inconsistency across different kernel drivers [2]. Some drivers may incorrectly report positive values for discharging or handle current direction differently due to hardware-specific limitations or legacy implementation choices [2][3]. The kernel community has actively worked to standardize this behavior to ensure that userspace applications can rely on a consistent sign convention [2][4]. If you encounter a driver that does not follow this convention, it is considered a driver-level issue or limitation rather than an ambiguity in the ABI specification itself [2][4]. Developers are encouraged to ensure drivers adhere to the "negative = discharging" convention to maintain consistency across the ecosystem [2][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant code and type declarations.
grep -nR "batteryCurrent\|batteryVoltage\|batteryPower" linux/Platform.c | head -n 40

echo
echo "--- linux/Platform.c around the reported lines ---"
sed -n '1010,1055p' linux/Platform.c

echo
echo "--- broader declarations in linux/Platform.c ---"
grep -nR "int64_t batteryCurrent\|uint64_t batteryVoltage\|batteryCurrent =" linux/Platform.c | head -n 40

Repository: htop-dev/htop

Length of output: 2834


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the Linux power-supply current parsing path and confirm the type flow.
rg -n "CURRENT_NOW|current_now|batteryCurrent|batteryVoltage" linux/Platform.c linux -g '!**/build/**' -g '!**/dist/**'

echo
echo "--- linux/Platform.c surrounding battery power computation ---"
sed -n '990,1050p' linux/Platform.c

Repository: htop-dev/htop

Length of output: 3792


Cast batteryVoltage to int64_t before multiplying. batteryCurrent may be negative for discharge, but mixing it with the unsigned voltage forces unsigned arithmetic and mangles the computed power. The current fix is just the cast on the right-hand operand.

Comment thread linux/Platform.c
Comment on lines +1071 to +1075
if (totalFull > 0) {
info->percent = ((double) totalRemain * 100.0) / (double) totalFull;
info->energyCurr = (double) totalRemain / 1000000.0;
info->energyFull = (double) totalFull / 1000000.0;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Capacity-only batteries still yield NAN percent.

When a sysfs battery exposes POWER_SUPPLY_CAPACITY but neither ENERGY_* nor CHARGE_* fulls (a valid kernel configuration), totalFull stays 0 and info->percent remains NAN, so Platform_getBattery() switches the method to BAT_ERR despite a usable percentage. Add a fallback that publishes batteryLevel directly when aggregated totals are unavailable.

Comment thread openbsd/Platform.c
Comment on lines 391 to 457
bool found = findDevice("acpibat0", mib, &snsrdev, &sdlen);

*percent = NAN;
if (found) {
bool haveTotalFull = false;
bool haveTotalRemain = false;
bool haveTotalPower = false;

int64_t totalFull = 0;
int64_t totalRemain = 0;
int64_t totalPower = 0;

/* See "sys/dev/acpi/acpibat.c" of OpenBSD source code for the indices
of the last field. */
mib[3] = SENSOR_WATTHOUR;
mib[4] = 0; /* "last full capacity" */
double last_full_capacity = 0;
bool haveBatteryFull = false;
int64_t batteryFull = 0;
if (sysctl(mib, 5, &s, &slen, NULL, 0) != -1)
last_full_capacity = s.value;
if (last_full_capacity > 0) {
batteryFull = s.value;

if (batteryFull > 0)
haveBatteryFull = true;

if (haveBatteryFull) {
mib[3] = SENSOR_WATTHOUR;
mib[4] = 3; /* "remaining capacity" */
if (sysctl(mib, 5, &s, &slen, NULL, 0) != -1) {
double charge = s.value;
*percent = 100 * (charge / last_full_capacity);
if (charge >= last_full_capacity) {
*percent = 100;
int64_t batteryRemain = s.value;
if (batteryRemain >= 0) {
totalRemain += batteryRemain;
totalFull += batteryFull;
haveTotalRemain = true;
haveTotalFull = true;
}
}
}

if (haveTotalRemain && haveTotalFull && totalFull > 0) {
info->percent = ((double) totalRemain * 100.0) / (double) totalFull;
if (totalRemain >= totalFull)
info->percent = 100;

info->energyCurr = (double) totalRemain / 1000000.0;
info->energyFull = (double) totalFull / 1000000.0;
}

mib[3] = SENSOR_INTEGER;
mib[4] = 0; /* "battery state" */
int64_t batteryState = 0;
if (sysctl(mib, 5, &s, &slen, NULL, 0) != -1)
batteryState = s.value;

mib[3] = SENSOR_WATTS;
mib[4] = 0; /* "rate" */
if (sysctl(mib, 5, &s, &slen, NULL, 0) != -1) {
int64_t batteryPower = s.value;
if (batteryState & 0x01)
batteryPower = -batteryPower;

totalPower += batteryPower;
haveTotalPower = true;
}

if (haveTotalPower) {
info->powerCurr = (double) totalPower / 1000000.0;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Aggregation only ever sees acpibat0.

The block builds totalFull/totalRemain/totalPower as if summing multiple packs, but findDevice("acpibat0", ...) restricts it to the first battery. On multi-battery hardware percent, Wh, and time estimates reflect only that pack. Enumerate all acpibat* sensordevs (iterate mib[2], match xname prefix "acpibat") and accumulate across matches, as discussed on the prior review thread.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (5)
linux/Platform.c (5)

844-847: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not discard the procfs percentage when AC detection fails.

Line 846 still forces percent = NAN whenever procAcpiCheck() returns AC_ERROR. /proc/acpi/battery/* can hold valid capacity data while /proc/acpi/ac_adapter/*/state is unreadable. In that case Platform_getBattery() abandons procfs for no reason. Read the percentage unconditionally and leave info->ac = AC_ERROR.

Proposed fix
 static void Platform_Battery_getProcData(BatteryInfo* info) {
    info->ac = procAcpiCheck();
-   info->percent = AC_ERROR != info->ac ? Platform_Battery_getProcBatInfo() : NAN;
+   info->percent = Platform_Battery_getProcBatInfo();
 }

1037-1048: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix the power sign: unsigned multiplication plus unconditional negation.

Two defects combine here:

  • Line 1038 multiplies batteryCurrent (int64_t) by batteryVoltage (uint64_t). The signed operand converts to unsigned, so a negative CURRENT_NOW produces a huge positive batteryPower.
  • Line 1044 then negates the result whenever STATUS is Discharging. Drivers that already report signed CURRENT_NOW get their sign inverted, while POWER_NOW is reported unsigned and does need the STATUS sign.

Derive the magnitude first, then apply the STATUS sign once. This keeps the contract in BatteryMeter.h (negative = discharging) valid for both driver styles.

Proposed fix
       if (!haveBatteryPower && haveBatteryCurrent && haveBatteryVoltage) {
-         batteryPower = (batteryCurrent * batteryVoltage) / 1000000;
+         batteryPower = ((int64_t)(llabs(batteryCurrent) * batteryVoltage)) / 1000000;
          haveBatteryPower = true;
       }

       if (haveBatteryPower) {
+         if (batteryPower < 0)
+            batteryPower = -batteryPower;
          if (batteryIsDischarging)
             batteryPower = -batteryPower;

1049-1063: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Aggregate every AC supply instead of keeping the first result.

Line 1050 returns early once info->ac differs from AC_ERROR, so readdir() order decides the outcome on systems with several mains supplies. An offline entry visited first pins info->ac to AC_ABSENT even when another supply is online. Line 1056 is also dead: the guard above guarantees info->ac is already AC_ERROR.

Let AC_PRESENT win over AC_ABSENT.

Proposed fix
       } else if (type == AC) {
-         if (info->ac != AC_ERROR)
-            goto next;
-
          char buffer[2];
          ssize_t r = Compat_readfileat(entryFd, "online", buffer, sizeof(buffer));
-         if (r < 1) {
-            info->ac = AC_ERROR;
+         if (r < 1)
             goto next;
-         }
-
-         if (buffer[0] == '0')
-            info->ac = AC_ABSENT;
-         else if (buffer[0] == '1')
+
+         if (buffer[0] == '1')
             info->ac = AC_PRESENT;
+         else if (buffer[0] == '0' && info->ac != AC_PRESENT)
+            info->ac = AC_ABSENT;
       }

1072-1076: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Capacity-only batteries still report NAN percent.

A power supply may expose POWER_SUPPLY_CAPACITY without any ENERGY_* or CHARGE_* attribute. That is a valid kernel configuration. totalFull then stays 0, info->percent stays NAN, and Platform_getBattery() promotes the method to BAT_ERR although a usable percentage was parsed. Publish batteryLevel as a fallback when the aggregated energy totals are unavailable.

This needs an accumulator for the parsed capacity values (for example levelSum and levelCount filled inside the BAT branch), then:

Proposed fix
    if (totalFull > 0) {
       info->percent = ((double) totalRemain * 100.0) / (double) totalFull;
       info->energyCurr = (double) totalRemain / 1000000.0;
       info->energyFull = (double) totalFull / 1000000.0;
+   } else if (levelCount > 0) {
+      /* no energy/charge attributes exposed: fall back to reported capacity */
+      info->percent = (double) levelSum / (double) levelCount;
    }

1099-1108: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Probe sysfs before procfs.

Platform_Battery_getProcData() fills only ac and percent. It never sets powerCurr, energyCurr, or energyFull. With the current BAT_PROC-first order, every host that exposes both interfaces keeps the new rate, capacity, and time-estimate fields at NAN, and BatteryMeter falls back to the percent-only output. Try sysfs first and keep procfs as the fallback for legacy setups.

Proposed fix
-   if (Platform_Battery_method == BAT_PROC) {
-      Platform_Battery_getProcData(&Platform_Battery_cache);
-      if (!isNonnegative(Platform_Battery_cache.percent))
-         Platform_Battery_method = BAT_SYS;
-   }
    if (Platform_Battery_method == BAT_SYS) {
       Platform_Battery_getSysData(&Platform_Battery_cache);
       if (!isNonnegative(Platform_Battery_cache.percent))
+         Platform_Battery_method = BAT_PROC;
+   }
+   if (Platform_Battery_method == BAT_PROC) {
+      Platform_Battery_getProcData(&Platform_Battery_cache);
+      if (!isNonnegative(Platform_Battery_cache.percent))
          Platform_Battery_method = BAT_ERR;
    }

The initial value of Platform_Battery_method must become BAT_SYS for this order to take effect.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1e830471-e1d8-4d47-a04e-bdd83ebfab6a

📥 Commits

Reviewing files that changed from the base of the PR and between f8fe874 and 55a56a0.

📒 Files selected for processing (1)
  • linux/Platform.c

BenBE and others added 11 commits August 17, 2026 00:07
Assisted-by: CodeRabbit
Assisted-by: GPT 5.4
Assisted-by: Raptor mini
Assisted-by: CodeRabbit
Assisted-by: GPT 5.4
Assisted-by: Raptor mini
Assisted-by: CodeRabbit
Assisted-by: GPT 5.4
Assisted-by: Raptor mini
Assisted-by: CodeRabbit
Assisted-by: GPT 5.4
Assisted-by: Raptor mini
Co-authored-by: Nathan Scott <nathans@redhat.com>
Assisted-by: GPT 5.4
DAssisted-by: Raptor mini
Assisted-by: GPT 5.4
Assisted-by: Raptor mini
Assisted-by: CodeRabbit
Assisted-by: GPT 5.4
Assisted-by: Raptor mini
Assisted-by: CodeRabbit
Assisted-by: GPT 5.4
Assisted-by: CodeRabbit
Assisted-by: GPT 5.4
Assisted-by: Raptor mini
Co-authored-by: Nathan Scott <nathans@redhat.com>
Assisted-by: CodeRabbit
Assisted-by: GPT 5.4
Assisted-by: Raptor mini
@BenBE
BenBE force-pushed the battery-extension branch from 55a56a0 to 98e6748 Compare August 16, 2026 22:08
@ravi-arnan

Copy link
Copy Markdown
Contributor

Tested the head of this branch (98e6748) on real Darwin hardware, since the thread so far has no
hardware measurements on the macOS side.

Machine: MacBookPro12,1, macOS 12.7.6, Intel, Apple clang 14.0.0 (Command Line Tools 14.2),
ncurses from the SDK. Battery pack bq20z451, 6830 mAh design, 6379 mAh full charge, 292 cycles.

Build: ./autogen.sh && ./configure && make -j2 completes with zero warnings under the
default warning set (-Wall -Wextra -Wcast-align -Wcast-qual -Wfloat-equal -Wimplicit-int-conversion -Wnull-dereference ...).

Runtime, one-column header, discharging on battery:

TEXT mode:   Battery: Using bat, discharging at 5.9W, 49.9/71.8Wh (73.0%), time remaining: 8h28m
BAR mode:    the bar carries the label   bat -7.3W @ 50.0/71.8Wh, 6h48m

Numbers cross-checked against the registry sampled at the same moment
(Voltage=11262 mV, Amperage=-652 mA, AppleRawCurrentCapacity=4437 mAh,
AppleRawMaxCapacity=6379 mAh): power -7.34 W, energyCurr 49.97 Wh, energyFull 71.84 Wh,
ETA 6.81 h. Every field the meter printed matches.


1. Amperage sign, answering your question at darwin/Platform.c:750

Measured rather than argued. Discharging, no adapter connected (ExternalConnected=No,
IsCharging=No), read exactly the way this PR reads it, CFNumberGetValue(ref, kCFNumberDoubleType, ...):

Amperage  -1571  -1542  -2289  -1580  -652  -523  mA     (six samples over ~15 min)

IOKit's higher level power source API agrees: kIOPSCurrentKey in the same power source
description is -461 mA while discharging. So on an Intel Mac, negative means energy leaving the
pack, which is the convention this PR already assumes and the one BatteryMeter.c uses to pick the
discharging branch. No sign flip needed for Darwin.

A trap worth a comment in the code: ioreg prints this exact property as

"Amperage" = 18446744073709551247

which is the same value read as unsigned. Through CoreFoundation it is -369. The CFNumber type is
kCFNumberSInt64Type. Anyone comparing ioreg output against this code will think there is an
overflow bug where there is none.

2. Key availability and units, confirmed on this machine

  • AppleRawCurrentCapacity and AppleRawMaxCapacity both exist and are in mAh (4437 and 6379),
    so the mAh path this PR uses works on Intel Macs.
  • The IOPS keys the code no longer uses for energy really are percentages here:
    Current Capacity = 73, Max Capacity = 100. Reading Wh out of them would have been wrong, so
    the switch to the AppleSmartBattery registry is the right call.
  • IsCharging and ExternalConnected come back as CFBoolean, not CFNumber. Nothing in this PR
    reads them, but if a later commit does, they need CFGetTypeID handling rather than
    CFNumberGetValue.

3. AppleSmartBattery refreshes about once a minute

Repeated samples return byte-identical values for 30 to 60 seconds and then step. So powerCurr on
Darwin is closer to a one minute average than an instantaneous reading, and the meter will lag a
load change by up to a minute. Not a defect, but it explains a stale-looking ETA right after
plugging or unplugging, and it means a very short benchmark will not show up in the wattage at all.

4. energyFull drifts because it is scaled by the instantaneous terminal voltage

Ten samples over about fifteen minutes, same battery, AppleRawMaxCapacity constant at 6379 mAh:

min max
Voltage 11022 mV 11319 mV
energyFull as computed 70.31 Wh 72.20 Wh

That is a 1.9 Wh (2.7%) swing in the number presented to the user as the pack's full capacity, with
no physical change in the pack. The sag is largest under CPU load, so the displayed "full capacity"
shrinks exactly when the machine gets busy.

The time estimate is not affected: energyCurr carries the same voltage factor, so voltage cancels
in energyCurr / powerCurr. It is only the two absolute Wh figures that move.

If you want a stable denominator, the fix is a nominal pack voltage instead of the live one. This
pack reports its cells individually, BatteryData.CellVoltage = (3646, 3727, 3727), so cell count
is available and a nominal 3.8 V per cell would give a constant. That does mean hardcoding a
chemistry constant, so it may not be worth it. Documenting the approximation in the existing comment
would also be a defensible answer. Your call, and either way the current code is not wrong, just
noisy.

5. At 80 columns the ETA is the first thing lost

TEXT mode, one column header, discharging. The full line is 85 characters, 74 of them after the
Battery: caption. Rendered at three widths:

cols=80    Battery: Using bat, discharging at 7.3W, 50.0/71.8Wh (73.0%), time remaining
cols=100   Battery: Using bat, discharging at 5.9W, 49.9/71.8Wh (73.0%), time remaining: 8h28m
cols=120   Battery: Using bat, discharging at 5.9W, 49.9/71.8Wh (73.0%), time remaining: 8h28m

At the default 80 column terminal the new number that is hardest to get anywhere else is precisely
the one that gets cut. ETA 8h28m or 8h28m left in place of time remaining: 8h28m would fit at
80 with room to spare.


Everything above is the discharge path. I will follow up with the charging side (sign of
Amperage with the adapter connected, the AC+bat label, and the 95% cutoff in the time-to-full
estimate) once I can measure it on the same machine.

@ravi-arnan

Copy link
Copy Markdown
Contributor

Follow-up with the charging path measured on the same machine (MacBookPro12,1, macOS 12.7.6, branch
head 98e6748).

Amperage sign with the adapter connected. Positive, as the PR assumes:

ExternalConnected = CFBoolean 1
IsCharging        = CFBoolean 1
Amperage          = +2652 mA, +2717 mA   (two samples)

Together with the discharging samples in my previous comment (-413 to -2289 mA with no adapter),
both directions are now measured on Intel hardware, and they match the convention in
BatteryMeter.c. Your reading of the SBS sign question at darwin/Platform.c:750 is correct for
Darwin.

Meter output while charging, TEXT mode at 110 columns:

  Battery: Using AC, charging at 32.3W, 53.7/75.8Wh (74.0%), time to full: 0h34m

Cross-checked against the registry at that instant (Voltage=11877 mV, Amperage=2717 mA,
AppleRawCurrentCapacity=4521 mAh, AppleRawMaxCapacity=6379 mAh): 32.27 W, 53.70 Wh, 75.76 Wh,
and (0.95 * 75.76 - 53.70) / 32.27 * 60 = 33.97 rounded up to 34 minutes. Every field matches.

The 95% cutoff is visible to the user. At that same moment macOS reported 0:41 remaining while
the meter said time to full: 0h34m. The seven minute gap is exactly the 5% tail this code skips.
Both numbers are defensible, but a user with both on screen will read the meter as wrong rather than
as deliberately conservative. That is the same point raised earlier in the thread about the label,
and this is what it looks like in practice. time to 95% in the label, or dropping the cutoff, both
resolve it.

One correction to my previous comment: the energyFull drift is larger than I reported. With
the adapter connected the pack voltage rises to 11877 mV, so the same 6379 mAh full charge capacity
now renders as 75.8 Wh, against 70.3 Wh measured while discharging under CPU load at
11022 mV. That is 5.5 Wh, 7.8%, not the 2.7% I measured on battery alone. In practice the
displayed full capacity of the pack jumps by more than 5 Wh the moment the charger goes in, which is
the sort of thing that gets reported as a bug. The time estimates stay correct throughout, since the
voltage factor cancels, so a fixed nominal voltage for the two Wh figures would only improve what is
displayed and change nothing else.

@BenBE

BenBE commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Thank you very much for the very detailed report.

The display format is actually still up for debate, as even as it is now, it packs very many information in a quite compact space, while still trying to stay comprehensible for a casual observer. I somewhat thought about the ETA label earlier, but it somehow didn't feel proper, while working on that part of the code. But as mentioned: I'm very much open for ideas on how to condense the text information better.

Which brings me to the 95% cutoff: That one has a bit of reasoning behind it, that's not directly documented in the code itself (apart from the calculations that randomly take the 0.95 factor. But to elaborate on the reasoning there are mostly two reasons: The first one is that the charging isn't done in a linear fashion and actually slows down the more charge is already put into the battery. This causes the charging time to always be an underestimate, and might cause the remaining charging time to "increase". The second one is related to the way batteries can be damaged if always kept at full 100% charge. That's why when looking at the charge of your battery even with AC connected, you will often see only like 97-98% charge. If the cutoff was at full 100%, this would cause the battery to show ridiculous ETA times for charging when in fact that BMS mostly keeps the charge at a near-constant level without over-charging the battery.

One test that might still be worth performing is putting the system under load with a charger that's too weak for the load. With Thinkpads you can usually operate (and charge) the system with a 65W charger, even when the system under load might take like 80W+. This will cause the battery to discharge despite AC being connected and should be reported by the meter accordingly.

Finally, regarding the voltage level: calculating from cell voltage to nominal voltage requires knowledge of the pack topology, which is an entirely different can of worms. Glad to integrate patches for this, but not as part of this first set of patches. This PR is complex enough as it is. And given that most BMS can't even make up their mind about how much capacity their pack actually has, doesn't make this value very reliable either. On my notebook the shown pack capacity changes roughly based on phase of the moon, zodiac sign, number of coffee mugs emptied since last kernel update, Wifi signal strength, and the approximate payout of the retirement plan …

@ravi-arnan

Copy link
Copy Markdown
Contributor

I tried the weak-charger test. It does not reproduce on this machine, so here is the negative result with numbers, plus two things the attempt turned up.

The undersized-charger case is not reachable here

Four spinners on a 4-thread i5 (MacBookPro12,1, macOS 12.7.6), ps reporting 399.7% total CPU, stock 60W MagSafe. Sampled AppleSmartBattery every 10s through baseline, 2 minutes of load, and cooldown:

idle    amperage=+2473 mA  29.4W  ext=1 charging=1
load    amperage=+2473 mA  29.4W  ext=1 charging=1
load    amperage=+2499 mA  29.8W  ext=1 charging=1
load    amperage=+2471 mA  29.6W  ext=1 charging=1
cool    amperage=+2471 mA  29.6W  ext=1 charging=1

The charge current does not even dip. A dual core i5 at full tilt is nowhere near 60W, so the adapter never runs short and the battery never discharges. Reproducing your Thinkpad case needs an adapter weaker than the load, and I do not have one for this machine.

For what it is worth the code path looks right by reading: BatteryMeter.c:90 prints Using AC+bat when info.ac == AC_PRESENT and isDischarging, and the compact label at :138 does the same. I want to be clear that this is reading rather than measurement, since I could not exercise it.

Reading it did raise one thing. isDischarging requires powerCurr <= -5.0 and isCharging requires >= +5.0, so a weak-adapter case that nets out to a small drain, say -3W, falls between them and prints Using AC, stable at ... while the pack is in fact slowly emptying. Your 65W-against-80W example would be well past the threshold and would read AC+bat correctly; it is only the mild version that reads as stable.

The percent and the Wh pair on the same line come from different subsystems

info.percent comes from IOPS, kIOPSCurrentCapacityKey / kIOPSMaxCapacityKey (darwin/Platform.c:724-731). energyCurr and energyFull come from AppleSmartBattery, AppleRawCurrentCapacity / AppleRawMaxCapacity scaled by voltage (:738-739). Those are two different notions of full, and they disagree. I watched a full charge, sampling every 2 minutes:

time raw Wh pair shown meter line
14:31 61.5/77.5Wh = 79.4% 83.0% charging at 29.9W, time to full: 0h25m
14:41 65.3/77.4Wh = 84.4% 89.0% charging at 14.9W, time to full: 0h34m
15:00 71.0/78.7Wh = 90.2% 95.0% charging at 15.1W, time to full: 0h16m
15:12 74.5/79.4Wh = 93.8% 99.0% charging at 12.4W, time to full: 0h05m
15:14 75.6/79.5Wh = 95.1% 100.0% charging at 11.8W, no ETA

The gap widens from 3.6 to 4.9 points across the run, so it is not a fixed offset a user could learn to discount. Cross-checked against pmset -g batt at one instant: raw was 5144/6379 = 80.6% while pmset, which reads the same IOPS source, said 85%.

The last row is the one I would fix. The meter prints

Battery: Using AC, charging at 11.8W, 75.6/79.5Wh (100.0%)

while FullyCharged is No and 11.8W is still going into the pack, and the Wh pair immediately to the left of that 100.0% is 95.1%. If the percent were derived from the same pair as the Wh figures, the line would be self-consistent, and the cutoff would fire at a displayed 95% rather than a displayed 100%.

A correction to my earlier comment. I wrote that every field matched after cross-checking. Each field does match its own source, but I never checked the percent against the Wh pair, and it did not match even then: 53.70/75.76 is 70.9% where the line displayed 74.0%. The discrepancy was sitting in my own data and I missed it.

Your non-linear charging point is right, and it starts earlier than 95%

This is the part of your reply I could measure, and it supports the design:

raw current power time to full
79.4% 2465 mA 29.9W 0h25m
80.6% 2222 mA 27.0W 0h25m
81.7% 1970 mA 23.9W 0h26m
82.7% 1720 mA 20.9W 0h28m
83.6% 1475 mA 17.9W 0h30m
84.3% 1231 mA 14.9W 0h34m

The ETA grows by nine minutes while the battery is filling, exactly the effect you described. After that the current holds near 15W and the estimate falls monotonically to 0h05m.

So the tail is real, but on this pack it is the 79 to 84 percent band, where the BMS ramps from 30W down to 15W, and the 95% cutoff does not cover it. Whatever the label ends up saying, a user watching this meter between 80 and 85 percent sees the remaining time going up.

One more data point for the nominal voltage question

energyFull drifted from 77.4Wh to 79.5Wh over this single charge, with no change to the pack, purely because the voltage rose from 11900 to about 12200 mV as it filled. That is on top of the 7.8% jump between AC and battery I reported earlier, and it is the same fix: a fixed nominal voltage for the two Wh figures would hold them steady and would not touch the time estimates, since the voltage factor cancels there.

Observation window was 14:31 to 15:14 local; I did not capture the transition into stable past that point.

@ravi-arnan

Copy link
Copy Markdown
Contributor

Addendum, because every number above came from the charging path and I do not want to leave the impression that this is charge-specific.

Same machine a few hours later, on battery with no adapter:

Battery: Using bat, discharging at 8.5W, 68.9/75.7Wh (96.0%)

68.9/75.7 is 91.0%. At that instant AppleRawCurrentCapacity / AppleRawMaxCapacity was 5807/6379 = 91.0%, and BatteryData.StateOfCharge in the same registry entry was 91. So all three AppleSmartBattery witnesses agree on 91 while IOPS, and pmset -g batt along with it, says 96%.

Two things follow. The divergence is in every branch of the meter rather than only the charging one. And it tracks charge level rather than direction: this discharging sample at raw 91.0% sits on the same curve as the charging samples at raw 90.2% (shown 95.0%) and raw 91.4% (shown 96.0%).

So whatever you settle on for the ETA and the label, the percent looks like one change in Platform_getBattery rather than anything per-branch in BatteryMeter.c.

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

Labels

BSD 🐡 Issues related to *BSD DragonflyBSD 🪰 DragonflyBSD related issues enhancement Extension or improvement to existing feature FreeBSD 👹 FreeBSD related issues Linux 🐧 Linux related issues MacOS 🍏 MacOS / Darwin related issues NetBSD 🎏 NetBSD related issues OpenBSD 🐡 OpenBSD related issues PCP PCP related issues Solaris Solaris, Illumos, OmniOS, OpenIndiana

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants