The battery cycle count that doesn't exist
I inherited an EliteBook 840 G2 last September. First thing I did was wipe the cracked Windows 11 install someone had put on it and drop Windows 10 back on. Since then I've been trying to keep it in decent shape - SSD kept under 80% full, kept cool, the usual. Battery life has been surprisingly solid for 2015 hardware: 3-3.5 hours on video, 1.5-2 on regular use. I wanted to actually track whether that was going to hold up over time, so I went looking for a battery wear report.
powercfg /batteryreport is supposed to be that report. On this machine it's useless.
The report that reports nothing
Every capacity field in the generated HTML comes back as -. Design capacity, full charge capacity, cycle count, all blank. The "Battery capacity history" and "Battery life estimates" sections exist as empty tables. The only thing the report actually logs is a timeline of suspend/resume events - which state, which source, nothing about the battery itself.
Windows builds this report from a background energy-estimation service that's supposed to snapshot battery state periodically. If that service never populated its log - driver issue, OS build issue, EC not answering the way Windows expects - powercfg just formats an empty table. It's not lying, it's reading nothing.
Checked with HWiNFO to confirm the battery itself was fine: 0% wear level, full charge capacity matching design capacity. So the hardware was healthy, the built-in tool just couldn't see it over time - only in the moment.
Building a replacement
If Windows won't log history, I'd log it myself. The same data powercfg reads eventually comes from WMI classes under root\WMI - BatteryFullChargedCapacity, BatteryStatus, BatteryCycleCount - that HWiNFO also pulls from directly. Nothing exotic, just poll them and append a line to a log file.
First pass in PowerShell, one query per run, against the Battery WMI provider classes under root\WMI:
$full = Get-CimInstance -Namespace root\WMI -ClassName BatteryFullChargedCapacity
$status = Get-CimInstance -Namespace root\WMI -ClassName BatteryStatus
$cycles = Get-CimInstance -Namespace root\WMI -ClassName BatteryCycleCount
First output was garbage - duplicated values, Charging=False False, a blank Design field. Some of these WMI classes intermittently return two instances instead of one, and unindexed property access on an array just concatenates both. Select-Object -First 1 on each query fixed it. BatteryStaticData (the class that holds DesignedCapacity) also threw Generic failure outright on some runs - turned out to be WBEM_E_PROVIDER_FAILURE (0x80041001), a documented WMI error for "provider started fine, then choked mid-query." Design capacity is supposed to be a fixed hardware constant, so I checked it against the physical battery label - 44,021 mWh. Two separate powercfg /batteryreport runs, close together in time, gave me 44,685 mWh and 44,291 mWh. Neither matched the label, and they didn't match each other. Whatever powercfg was reading from, it wasn't stable and it wasn't the label. I hardcoded the label value going forward and stopped trusting any live query for this field - more on why in the next section.
Eventually rewrote the whole thing in C#, compiled with the built-in csc.exe, no Visual Studio or NuGet involved - a single battery.exe that appends one line per run to a log file:
[2026-07-23 15:01:15] Design=44021mWh Full=44494mWh Remaining=39555mWh Voltage=11794mV ChargeRate=0mW DischargeRate=12949mW Cycles=0 Charging=False
Scheduled it to run daily. This worked. Full wobbled a bit day to day - noise from the fuel gauge re-estimating on partial charge/discharge cycles, nothing alarming - but the numbers tracked with what HWiNFO showed live, and I finally had a history instead of a single snapshot.
The number that wouldn't move
Three weeks of daily logs in, one field never changed: Cycles=0. Every single entry. Meanwhile the Remaining values showed clear repeated deep-discharge-then-recharge behavior - days dropping to 15-20% before a full recharge, over and over.
A cycle counter that never increments despite that pattern isn't normal. Either the counter genuinely wasn't tracking anything, or something in my query chain was reading the wrong place.
Ruled out the easy explanations first. Enumerated every battery-related WMI class on the system:
Get-CimClass -Namespace root\WMI | Where-Object { $_.CimClassName -like "*Battery*" }
Ten classes, three I hadn't tried: MSBatteryClass, BatteryRuntime, BatteryTemperature. Dumped all of them. MSBatteryClass confirmed CycleCount: 0 from a second, independent provider path - same result, not a fluke of one specific query. BatteryTemperature returned nothing at all, no error, no object - the sensor isn't exposed. BatteryRuntime returned 4294967295, which is 0xFFFFFFFF, the standard ACPI sentinel for "value unavailable," not a real number.
Cross-checked against Win32_Battery in the completely separate root\cimv2 namespace, a different provider entirely:
Get-CimInstance -Class Win32_Battery | Format-List *
DesignCapacity and FullChargeCapacity both blank. EstimatedRunTime: 71582788 - Microsoft's own documented sentinel value for "not currently calculated." Same story from a totally different code path. Every layer I checked agreed with every other layer: no cycle data, anywhere, through any provider.
That consistency was itself the interesting result. If one tool disagreed with the others, I'd suspect that one tool. When all of them agree, the problem isn't in any of the tools - it's further down.
Going to the source
Underneath WMI, underneath the driver, the actual authority on what a battery reports to Windows is an ACPI method implemented in the laptop's own firmware. Two methods matter here, both defined in the ACPI specification: _BIF (Battery Information), the original method from early ACPI, and _BIX (Battery Information Extended), added in ACPI 4.0 specifically to add fields the first version never had - including cycle count.
If I could see which one this hardware actually implements, I'd have a real answer instead of an inferred one.

Pulled the DSDT table (the ACPI definition block firmware exposes to the OS) using RW-Everything, a free low-level hardware inspection tool, then disassembled it with iasl, the official disassembler/compiler from Intel's ACPICA project:
iasl.exe -d dsdt.bin
That produces a plain-text .dsl file - the actual decompiled firmware source for how this laptop describes its own hardware to the OS. Searched it for both method names:
Select-String -Path .\dsdt.dsl -Pattern "_BIX|_BIF|CycleCount|CYCT" -Context 5,15
_BIX never appeared, not even commented out or partially implemented. _BIF matched twice - once per battery slot. This firmware only ever implements the older method. _BIX isn't hidden or broken here; it simply doesn't exist on this hardware.
Reading the actual package
_BIF returns a fixed 13-field package. Found the implementation feeding it:
Method (BTIF, 1, Serialized)
{
Local0 = \_SB.PCI0.LPCB.EC0.BTIF (Arg0)
If ((Local0 == 0xFF))
{
Return (Package (0x0D)
{
0x00, // Power Unit
0xFFFFFFFF, // Design Capacity
0xFFFFFFFF, // Last Full Charge Capacity
0x01, // Battery Technology
0xFFFFFFFF, // Design Voltage
0x00, // Design Capacity of Warning
0x00, // Design Capacity of Low
0x00, // Battery Capacity Granularity 1
0x00, // Battery Capacity Granularity 2
"", // Model Number
"", // Serial Number
"", // Battery Type
0x00 // OEM Information
})
}
Else
{
Return (DerefOf (NBTI [Arg0]))
}
}
Thirteen fields, zero-indexed. That's the entire package _BIF is capable of returning, spec-defined, no hidden thirteenth-plus slot. Cycle count isn't one of the fields - not omitted by HP, not zeroed out by a bug, just structurally absent from the method this firmware speaks. Every tool built on top of this - WMI, Win32_Battery, powercfg, HWiNFO - was reporting the honest truth the entire time. Cycles=0 wasn't a broken read. It was the only correct answer available.
- One side note worth keeping
- in the real (non-error) branch, both Design Capacity and Last Full Charge get written from the same source, `BFC` (battery full charge), at initialization. That explains a smaller mystery from earlier in this whole process - why `powercfg /energy`'s trace and some live queries showed Design Capacity drifting to match Full Charge instead of staying fixed. It's not measurement noise, it's this firmware seeding one field from the other on at least one code path. Reason enough to keep the design capacity value hardcoded from the one legitimate reading, rather than trusting any live query for it.
Why this generation specifically
_BIX isn't rare or experimental - it's over 15 years old at this point. The real forcing function for adoption was Windows 8's Connected Standby / Modern Standby certification, which needed the richer telemetry _BIX provides and made it a practical requirement for consumer ultrabooks chasing that certification starting around 2012.
Business-line laptops that weren't targeting instant-wake sleep states didn't have the same pressure to update. A 2015 corporate workhorse like the 840 G2 kept EC firmware designs that predate the practical push toward _BIX, because _BIF still technically worked and there was no certification forcing the change. Newer hardware - including later EliteBook generations - mostly doesn't have this gap. This is specifically an artifact of hardware age and market segment, not something you'd hit on a modern machine.
The cache that drifts
BatteryStaticData kept throwing Generic failure - HRESULT 0x80041001, WBEM_E_PROVIDER_FAILURE - intermittently, not consistently. Checked Windows Event Viewer's WMI-Activity log hoping to catch the actual provider error. Every error I found there was unrelated (IIS feature checks, touchpad driver queries, a different client process entirely). Reproduced the failure live, refreshed the log immediately after - nothing new appeared. The failure isn't reaching that log at all.
What actually settled it was a more direct test: force a live failure, then immediately generate a powercfg report and see if it inherited the error.
while ($true) { try { Get-CimInstance -Namespace root\wmi -ClassName BatteryStaticData -ErrorAction Stop | Out-Null } catch { Write-Host $_.Exception.Message; break } }
It failed instantly - Generic failure on the first attempt. Ran powercfg /batteryreport right after, in that same window. The report generated cleanly, full header populated, no trace of the failure I'd just proven was happening live.
That rules out "powercfg calls this WMI class and sometimes gets unlucky." If it were reading live, a proven failure seconds earlier should have shown up in the report. It didn't. powercfg's header is coming from somewhere other than a live BatteryStaticData call - some cache, refreshed on its own schedule, not touched at report-generation time.
The two mismatched capacity readings above are the other half of the evidence: a genuinely frozen cache would return the same number every time, since design capacity shouldn't change at all. Instead it drifted between runs. So whatever this is, it's periodically refreshed, not fetched live and not permanently fixed either - which also explains why the fallback theory ("the EC is just busy with other things") never quite fit: that would predict occasional live failures with no pattern, not a cache that's stable moment-to-moment but drifts release over release.
I tried to find exactly where that cache lives. Ruled out SRUM's energy-estimation data (powercfg /srumutil - it tracks per-app power consumption, no DesignCapacity field anywhere in that schema). Searched the registry under both the ACPI device enumeration tree and the battery class driver's Device Parameters key, comparing decoded numeric values rather than raw text to avoid missing anything stored as a binary DWORD. Nothing. At that point the honest options left were WMI's own binary repository (which registry tools can't see into at all) or an actual Process Monitor capture to watch powercfg.exe touch the filesystem/registry live - a heavier investigation than felt worth it for a field I'd already solved a workaround for.
So: proven cached, proven to drift, exact storage location still unknown. I'm hardcoding the physical label value and treating any live capacity query on this machine as unreliable by default - not because I know exactly why, but because I've now shown twice that "why" doesn't change what to do about it.
Where it landed
The logger works, hardcoded design capacity holds up under every independent check I ran against it, and cycle count is now understood as a genuine architectural absence rather than a bug to keep chasing. Real cycle tracking isn't recoverable on this hardware through any software layer, because ACPI itself never asks the EC for that value here.
What I do log instead going forward: total discharged mWh accumulated across runs, divided by design capacity, as a rough self-computed equivalent-cycle count. Not the real thing, but a real number derived from real behavior - which is more than the built-in tool ever gave me.
A note on process
Most of the investigative back-and-forth here happened in conversation with Claude - talking through what each failed WMI query actually implied, ruling out explanations one at a time, and figuring out which tool to reach for next when the current one hit a wall. It's also where the _BIF/_BIX distinction first came up as something worth checking directly rather than staying a guess, and where the ACPI dump-and-disassemble path got mapped out step by step (RW-Everything for the table, ACPICA's iasl to decompile it, then reading the actual _BIF package by hand to confirm cycle count isn't a field at all). The C# rewrite and the config-driven refactor that followed went through the same back-and-forth before landing where they did. None of it replaced doing the work - running the tools, reading the output, deciding what it meant was still the actual investigation - but having something to think out loud with made the dead ends a lot faster to rule out.