Skip to content

Fix Windows battery remaining capacity percent not reflecting IOCTL#3124

Merged
dbwiddis merged 1 commit intooshi:masterfrom
dbwiddis:fix-win-battery
Apr 7, 2026
Merged

Fix Windows battery remaining capacity percent not reflecting IOCTL#3124
dbwiddis merged 1 commit intooshi:masterfrom
dbwiddis:fix-win-battery

Conversation

@dbwiddis
Copy link
Copy Markdown
Member

@dbwiddis dbwiddis commented Apr 7, 2026

Value was never updated; latent bug from 10 years ago became visible with Win11 23H2 nerfing of QueryNTPowerInformation

Fixes #3108

Summary by CodeRabbit

  • Bug Fixes
    • Fixed Windows battery remaining capacity percentage calculation to match system-reported values.
    • When a battery is charging, time-remaining is now reported as unknown instead of an incorrect estimate.
    • Improved instant and estimated time calculations so discharging batteries show more accurate remaining-time values.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 7, 2026

📝 Walkthrough

Walkthrough

This pull request removes the SystemBatteryState CallNtPowerInformation query and derives battery timing, capacity-percent, and rate/estimated-time exclusively from per-battery DeviceIoControl results. Charging sets estimated time to -2, remaining percent is recalculated from current/max capacity, and instant-time fallback logic was adjusted.

Changes

Cohort / File(s) Summary
Documentation
CHANGELOG.md
Added changelog entry documenting PR [#3124] for a Windows battery remaining-capacity percent fix.
Windows Battery Metrics
oshi-core/src/main/java/oshi/hardware/platform/windows/WindowsPowerSource.java
Removed CallNtPowerInformation(SystemBatteryState, ...); now uses per-battery DeviceIoControl results only. Recomputes psRemainingCapacityPercent as psCurrentCapacity / psMaxCapacity (clamped), forces psTimeRemainingEstimated = -2d when charging, adjusts instant-time recompute condition to <= 0, and assigns estimated time from instant time when discharging. Removed related imports/usages.

Sequence Diagram(s)

sequenceDiagram
    participant App as App (asks HAL)
    participant WindowsPS as WindowsPowerSource
    participant Device as Battery Device (DeviceIoControl)
    participant Parser as Parser/Calculator

    App->>WindowsPS: request getPowerSources()/updateAttributes()
    WindowsPS->>Device: enumerate battery devices (DeviceIoControl)
    Device-->>WindowsPS: per-battery status/capacity/rate
    WindowsPS->>Parser: parse values, compute percent, instant time
    Parser-->>WindowsPS: psRemainingCapacityPercent, psTimeRemainingInstant
    WindowsPS->>WindowsPS: if charging => psTimeRemainingEstimated = -2
    WindowsPS->>WindowsPS: if discharging and instant>0 => psTimeRemainingEstimated = instant
    WindowsPS-->>App: return updated PowerSource objects
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I nibble code beneath the moon,
Per-battery checks hum a new tune.
Percent no longer stuck at one,
DeviceIoControl gets it done,
Windows batteries hop back to tune ⚡

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: fixing Windows battery remaining capacity percent to properly reflect IOCTL values.
Linked Issues check ✅ Passed The PR successfully addresses issue #3108 by updating WindowsPowerSource.java to derive remaining capacity percent from DeviceIoControl results and recalculating it as psCurrentCapacity/psMaxCapacity, fixing the reported battery percent issue.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the Windows battery remaining capacity percent issue; CHANGELOG.md update and WindowsPowerSource.java modifications are both within scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📋 Issue Planner

Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).

View plan for ticket: #3108

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

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 and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 7, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 67.70%. Comparing base (6bb1fd9) to head (fe8dd93).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3124      +/-   ##
==========================================
+ Coverage   66.66%   67.70%   +1.03%     
==========================================
  Files          37       37              
  Lines        2025     2025              
  Branches      336      336              
==========================================
+ Hits         1350     1371      +21     
+ Misses        555      539      -16     
+ Partials      120      115       -5     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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 (2)
oshi-core/src/main/java/oshi/hardware/platform/windows/WindowsPowerSource.java (2)

75-77: ⚠️ Potential issue | 🟠 Major

Don't turn a single System Battery into a per-pack percentage.

getPowerSources() still returns one System Battery, but Line 271 stops after the first enumerated battery and Line 200 now exposes that pack's current/max ratio as the public remaining-capacity percent. If Windows exposes more than one battery, callers will get a per-pack number instead of the system-wide percentage. Please accumulate current/max across all batteries before setting psRemainingCapacityPercent.

Also applies to: 200-201, 270-271

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@oshi-core/src/main/java/oshi/hardware/platform/windows/WindowsPowerSource.java`
around lines 75 - 77, getPowerSources() currently returns a single "System
Battery" and the code that computes psRemainingCapacityPercent (in
getPowerSource/getPowerSources logic) stops after the first enumerated pack,
causing per-pack percentages to be exposed; change the logic to enumerate all
batteries, return a List of PowerSource objects (one system-level entry if
that's the intended public API) and—critically—when computing
psRemainingCapacityPercent (in the method(s) that read battery entries, e.g.,
getPowerSource and its battery enumeration loop) sum the available/current
capacities and the full/max capacities across all detected battery packs and
compute remainingPercent = totalCurrent / totalMax instead of using the first
pack only; also remove any early break that stops after the first battery so all
packs are included in the aggregation.

251-269: ⚠️ Potential issue | 🟠 Major

The new discharging estimate can publish invalid fallback values.

If the BatteryEstimatedTime query fails, psTimeRemainingInstant is still 0d, and Line 268 exposes that as the estimated value for a discharging battery. Even when Lines 260-265 run, the fallback uses psMaxCapacity - psCurrentCapacity, which is time-to-full, not time-to-empty, so the new discharging path can report the wrong number. That violates oshi-core/src/main/java/oshi/hardware/PowerSource.java:51-69 and the existing assertion in oshi-core/src/test/java/oshi/hardware/PowerSourceTest.java:27-40.

💡 Suggested adjustment
-                                                        // Fallback
-                                                        if (psTimeRemainingInstant < 0 && psPowerUsageRate != 0) {
-                                                            psTimeRemainingInstant = (psMaxCapacity - psCurrentCapacity)
-                                                                    * 3600d / psPowerUsageRate;
-                                                            if (psTimeRemainingInstant < 0) {
-                                                                psTimeRemainingInstant *= -1;
-                                                            }
-                                                        }
-                                                        if (psDischarging) {
+                                                        // Fallback
+                                                        if (psTimeRemainingInstant <= 0 && psPowerUsageRate != 0) {
+                                                            double capacityDelta = psDischarging ? psCurrentCapacity
+                                                                    : psMaxCapacity - psCurrentCapacity;
+                                                            psTimeRemainingInstant = capacityDelta * 3600d
+                                                                    / Math.abs(psPowerUsageRate);
+                                                        }
+                                                        if (psDischarging && psTimeRemainingInstant > 0d) {
                                                             psTimeRemainingEstimated = psTimeRemainingInstant;
                                                         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@oshi-core/src/main/java/oshi/hardware/platform/windows/WindowsPowerSource.java`
around lines 251 - 269, The fallback logic in WindowsPowerSource that computes
psTimeRemainingInstant can produce wrong (time-to-full) values when reporting a
discharging battery and can publish zero as a valid estimate; update the
fallback so it only runs when the BatteryEstimatedTime query truly failed
(psTimeRemainingInstant not set/negative) and psPowerUsageRate != 0, and compute
time-to-empty when psDischarging is true (use psCurrentCapacity * 3600d /
psPowerUsageRate) and time-to-full when not discharging (use (psMaxCapacity -
psCurrentCapacity) * 3600d / psPowerUsageRate); also ensure you only assign
psTimeRemainingEstimated when the computed psTimeRemainingInstant is a
positive/valid value. Reference: WindowsPowerSource class and the variables
psTimeRemainingInstant, psDischarging, psPowerUsageRate, psMaxCapacity,
psCurrentCapacity, and psTimeRemainingEstimated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@oshi-core/src/main/java/oshi/hardware/platform/windows/WindowsPowerSource.java`:
- Around line 75-77: getPowerSources() currently returns a single "System
Battery" and the code that computes psRemainingCapacityPercent (in
getPowerSource/getPowerSources logic) stops after the first enumerated pack,
causing per-pack percentages to be exposed; change the logic to enumerate all
batteries, return a List of PowerSource objects (one system-level entry if
that's the intended public API) and—critically—when computing
psRemainingCapacityPercent (in the method(s) that read battery entries, e.g.,
getPowerSource and its battery enumeration loop) sum the available/current
capacities and the full/max capacities across all detected battery packs and
compute remainingPercent = totalCurrent / totalMax instead of using the first
pack only; also remove any early break that stops after the first battery so all
packs are included in the aggregation.
- Around line 251-269: The fallback logic in WindowsPowerSource that computes
psTimeRemainingInstant can produce wrong (time-to-full) values when reporting a
discharging battery and can publish zero as a valid estimate; update the
fallback so it only runs when the BatteryEstimatedTime query truly failed
(psTimeRemainingInstant not set/negative) and psPowerUsageRate != 0, and compute
time-to-empty when psDischarging is true (use psCurrentCapacity * 3600d /
psPowerUsageRate) and time-to-full when not discharging (use (psMaxCapacity -
psCurrentCapacity) * 3600d / psPowerUsageRate); also ensure you only assign
psTimeRemainingEstimated when the computed psTimeRemainingInstant is a
positive/valid value. Reference: WindowsPowerSource class and the variables
psTimeRemainingInstant, psDischarging, psPowerUsageRate, psMaxCapacity,
psCurrentCapacity, and psTimeRemainingEstimated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f8ace6af-4a6c-45ac-b858-c6ee49eb6e06

📥 Commits

Reviewing files that changed from the base of the PR and between 6bb1fd9 and 740294f.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • oshi-core/src/main/java/oshi/hardware/platform/windows/WindowsPowerSource.java
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Codacy Static Code Analysis
  • GitHub Check: Test JDK 21, macos-latest
  • GitHub Check: Test JDK 11, windows-latest
  • GitHub Check: Test JDK 11, macos-latest
  • GitHub Check: Test JDK 25, macos-latest
  • GitHub Check: Analyze (java)
  • GitHub Check: Test JDK 25, windows-latest
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-03-29T20:01:06.452Z
Learnt from: dbwiddis
Repo: oshi/oshi PR: 3014
File: oshi-core-java11/src/main/java/module-info.java:10-17
Timestamp: 2026-03-29T20:01:06.452Z
Learning: In the oshi project, `oshi-core-java25/src/main/java/module-info.java` intentionally does NOT export `oshi.util.platform.unix.freebsd`, `oshi.util.platform.unix.openbsd`, or `oshi.util.platform.unix.solaris`, because the FFM (Foreign Function & Memory) implementation only supports Windows, macOS, and Linux. This differs from `oshi-core-java11/src/main/java/module-info.java` which exports all Unix platform packages. The asymmetry is by design.

Applied to files:

  • oshi-core/src/main/java/oshi/hardware/platform/windows/WindowsPowerSource.java
📚 Learning: 2026-04-06T04:45:47.644Z
Learnt from: dbwiddis
Repo: oshi/oshi PR: 3121
File: oshi-core/src/main/java/oshi/software/os/linux/LinuxOperatingSystem.java:53-53
Timestamp: 2026-04-06T04:45:47.644Z
Learning: In the oshi/oshi project, only interfaces and classes in the root packages `oshi`, `oshi.hardware`, and `oshi.software.os` are considered the public API. Code in subpackages that represent specific implementations/platforms (e.g., `oshi.software.os.linux`, `oshi.hardware.platform.linux`, and similar subpackages) is implementation detail. When reviewing, do not treat changes to classes in these platform-specific subpackages as public API breaks (for example: making `LinuxOperatingSystem` abstract, narrowing constructor visibility, or adding/removing abstract methods should not be flagged as breaking public API).

Applied to files:

  • oshi-core/src/main/java/oshi/hardware/platform/windows/WindowsPowerSource.java
🔇 Additional comments (2)
oshi-core/src/main/java/oshi/hardware/platform/windows/WindowsPowerSource.java (1)

189-191: Good sentinel handling for charging.

Line 191 maps a charging battery to the documented -2d "unlimited" value, which keeps getTimeRemainingEstimated() aligned with the cross-platform contract.

CHANGELOG.md (1)

9-9: Changelog entry looks accurate.

This note captures the user-visible behavior change clearly and makes the Windows-specific fix easy to find in the next release notes.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
oshi-core/src/main/java/oshi/hardware/platform/windows/WindowsPowerSource.java (1)

200-201: Core fix looks correct, with a minor edge-case consideration.

This correctly calculates the remaining capacity percentage from the IOCTL-provided values, addressing the main bug. The Math.min(1d, ...) properly ensures the upper bound per the interface contract.

One edge case: if bi.FullChargedCapacity (assigned to psMaxCapacity at line 172) were ever 0, this would produce Infinity. While a real battery should never report 0 for full capacity, a defensive check could guard against malformed data:

if (psMaxCapacity > 0) {
    psRemainingCapacityPercent = Math.min(1d, (double) psCurrentCapacity / psMaxCapacity);
}

This is likely a rare scenario, so flagging as optional.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6a19169e-f6a7-4a03-8f53-b7c334424797

📥 Commits

Reviewing files that changed from the base of the PR and between 740294f and fe8dd93.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • oshi-core/src/main/java/oshi/hardware/platform/windows/WindowsPowerSource.java
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Codacy Static Code Analysis
  • GitHub Check: Test JDK 25, macos-latest
  • GitHub Check: Test JDK 11, windows-latest
  • GitHub Check: Test JDK 11, macos-latest
  • GitHub Check: Test JDK 25, windows-latest
  • GitHub Check: Test JDK 21, macos-latest
  • GitHub Check: Test JDK 21, windows-latest
  • GitHub Check: Analyze (java)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-03-29T20:01:06.452Z
Learnt from: dbwiddis
Repo: oshi/oshi PR: 3014
File: oshi-core-java11/src/main/java/module-info.java:10-17
Timestamp: 2026-03-29T20:01:06.452Z
Learning: In the oshi project, `oshi-core-java25/src/main/java/module-info.java` intentionally does NOT export `oshi.util.platform.unix.freebsd`, `oshi.util.platform.unix.openbsd`, or `oshi.util.platform.unix.solaris`, because the FFM (Foreign Function & Memory) implementation only supports Windows, macOS, and Linux. This differs from `oshi-core-java11/src/main/java/module-info.java` which exports all Unix platform packages. The asymmetry is by design.

Applied to files:

  • oshi-core/src/main/java/oshi/hardware/platform/windows/WindowsPowerSource.java
📚 Learning: 2026-04-06T04:45:47.644Z
Learnt from: dbwiddis
Repo: oshi/oshi PR: 3121
File: oshi-core/src/main/java/oshi/software/os/linux/LinuxOperatingSystem.java:53-53
Timestamp: 2026-04-06T04:45:47.644Z
Learning: In the oshi/oshi project, only interfaces and classes in the root packages `oshi`, `oshi.hardware`, and `oshi.software.os` are considered the public API. Code in subpackages that represent specific implementations/platforms (e.g., `oshi.software.os.linux`, `oshi.hardware.platform.linux`, and similar subpackages) is implementation detail. When reviewing, do not treat changes to classes in these platform-specific subpackages as public API breaks (for example: making `LinuxOperatingSystem` abstract, narrowing constructor visibility, or adding/removing abstract methods should not be flagged as breaking public API).

Applied to files:

  • oshi-core/src/main/java/oshi/hardware/platform/windows/WindowsPowerSource.java
🔇 Additional comments (3)
oshi-core/src/main/java/oshi/hardware/platform/windows/WindowsPowerSource.java (3)

102-104: Good documentation of the rationale.

The comment clearly explains why the previous approach using CallNtPowerInformation(SystemBatteryState) was removed and provides context about the Windows 11 23H2 behavior change. This will help future maintainers understand the design decision.


189-192: Correct sentinel value for charging state.

Setting psTimeRemainingEstimated = -2d when BATTERY_CHARGING is detected aligns with the PowerSource interface contract where -2.0 means "unlimited", and the AbstractPowerSource.formatTimeRemaining() method correctly interprets values < -1.5 as "Charging".


259-269: Fallback logic is sound.

The changes correctly handle time estimation:

  1. Changing the condition to <= 0 appropriately triggers the fallback when the IOCTL returns 0 (no estimate available), not just negative values.

  2. The discharge/charge formulas correctly compute time from capacity and power rate.

  3. The guard psPowerUsageRate != 0 prevents division by zero.

  4. When charging, psTimeRemainingInstant gets updated with the time-to-full estimate, while psTimeRemainingEstimated remains -2d (set at line 191), which correctly displays as "Charging" per the interface contract.

@dbwiddis dbwiddis merged commit f274b23 into oshi:master Apr 7, 2026
16 checks passed
@dbwiddis dbwiddis deleted the fix-win-battery branch April 7, 2026 18:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Window 11 battery percent always 100%

1 participant