Fix Windows battery remaining capacity percent not reflecting IOCTL#3124
Fix Windows battery remaining capacity percent not reflecting IOCTL#3124dbwiddis merged 1 commit intooshi:masterfrom
Conversation
📝 WalkthroughWalkthroughThis pull request removes the SystemBatteryState Changes
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 📋 Issue PlannerLet us write the prompt for your AI agent so you can ship faster (with fewer bugs). View plan for ticket: ✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify 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. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
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 | 🟠 MajorDon't turn a single
System Batteryinto a per-pack percentage.
getPowerSources()still returns oneSystem Battery, but Line 271 stops after the first enumerated battery and Line 200 now exposes that pack'scurrent/maxratio 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 settingpsRemainingCapacityPercent.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 | 🟠 MajorThe new discharging estimate can publish invalid fallback values.
If the
BatteryEstimatedTimequery fails,psTimeRemainingInstantis still0d, and Line 268 exposes that as the estimated value for a discharging battery. Even when Lines 260-265 run, the fallback usespsMaxCapacity - psCurrentCapacity, which is time-to-full, not time-to-empty, so the new discharging path can report the wrong number. That violatesoshi-core/src/main/java/oshi/hardware/PowerSource.java:51-69and the existing assertion inoshi-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
📒 Files selected for processing (2)
CHANGELOG.mdoshi-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 keepsgetTimeRemainingEstimated()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.
740294f to
fe8dd93
Compare
There was a problem hiding this comment.
🧹 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 topsMaxCapacityat line 172) were ever 0, this would produceInfinity. 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
📒 Files selected for processing (2)
CHANGELOG.mdoshi-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 = -2dwhenBATTERY_CHARGINGis detected aligns with thePowerSourceinterface contract where -2.0 means "unlimited", and theAbstractPowerSource.formatTimeRemaining()method correctly interprets values < -1.5 as "Charging".
259-269: Fallback logic is sound.The changes correctly handle time estimation:
Changing the condition to
<= 0appropriately triggers the fallback when the IOCTL returns 0 (no estimate available), not just negative values.The discharge/charge formulas correctly compute time from capacity and power rate.
The guard
psPowerUsageRate != 0prevents division by zero.When charging,
psTimeRemainingInstantgets updated with the time-to-full estimate, whilepsTimeRemainingEstimatedremains-2d(set at line 191), which correctly displays as "Charging" per the interface contract.
Value was never updated; latent bug from 10 years ago became visible with Win11 23H2 nerfing of QueryNTPowerInformation
Fixes #3108
Summary by CodeRabbit