# Read S0ix residency and drain from powercfg /sleepstudy

> Turn a powercfg /sleepstudy report into per-session hardware low-power residency (HW DRIPS), drain in mW and %/h, with two small Python scripts.

Published 2026-11-11. HTML version: https://guides.tsvinc.dev/sleepstudy-s0ix-residency-drain/


This turns the HTML report from `powercfg /sleepstudy` into one line per sleep on battery: how much of it the SoC spent in its deepest hardware idle state (S0ix, which Windows calls hardware DRIPS), and how much battery it used, in mWh, mW and percent per hour.

> **Scope** — Verified on: ASUS Zenbook 14 OLED UX3405CA, Intel Core Ultra 9 285H, BIOS UX3405CA.316, Windows 11 Pro 10.0.26200 (build 26200.9445), Python 3.12.10 (2026-11-10, on sleeps recorded 2026-11-06, 2026-11-07 to 09 and 2026-11-09).
> **Not verified:** any other laptop or SoC; AMD machines (Microsoft says hardware DRIPS is reported on Intel and Qualcomm SoCs only); machines that sleep in S3 instead of Modern Standby; running `powercfg /sleepstudy` from a non-elevated shell; any Python version other than 3.12.10.
> **Needs:** a Modern Standby laptop with a battery, an elevated PowerShell or Command Prompt, Python 3 (standard library only), the two scripts from the [appendix](#appendix-the-two-scripts).
> **Time:** one night with the lid closed on battery, then about 10 minutes. **Risk:** none: no step changes a setting. `powercfg /sleepstudy` writes one HTML file, and the scripts only read it.

## Before you start

- Make a working folder and save the two scripts from the [appendix](#appendix-the-two-scripts) in it as `residency.py` and `blockers.py`, side by side: `residency.py` imports its HTML parser from `blockers.py`. Run them with `python -I` (isolated mode: user site-packages and the current directory stay off the import path; the script adds its own folder itself).
- Check Python:

```console
PS> python --version
Python 3.12.10
```

- Backups: none needed. Nothing is changed; keep each night's HTML under its own name (`sleepstudy-<date>.html`) so a later re-run reads the same data.
- Placeholders: none. The commands below run inside the working folder; in the pasted output it appears as `C:\Users\user\sleepstudy` (my user name and folder path are replaced).

## Steps

1. **Check that the laptop sleeps in Modern Standby.**

   ```console
   PS> powercfg /a
   The following sleep states are available on this system:
       Standby (S0 Low Power Idle) Network Connected
       Hibernate
       Fast Startup

   The following sleep states are not available on this system:
       Standby (S1)
   	The system firmware does not support this standby state.
   	This standby state is disabled when S0 low power idle is supported.

       Standby (S2)
   	The system firmware does not support this standby state.
   	This standby state is disabled when S0 low power idle is supported.

       Standby (S3)
   	This standby state is disabled when S0 low power idle is supported.

       Hybrid Sleep
   	Standby (S3) is not available.
   	The hypervisor does not support this standby state.
   ```

   Expected: `Standby (S0 Low Power Idle)` under "available". Here S3 is listed as "disabled when S0 low power idle is supported", so every sleep on this laptop is a Modern Standby sleep, and SleepStudy records it. If your list shows `Standby (S3)` as available instead, this manual does not apply.

2. **Check when the laptop will hibernate on battery.**

   ```console
   PS> powercfg /q SCHEME_CURRENT SUB_SLEEP HIBERNATEIDLE
   Power Scheme GUID: 381b4222-f694-41f0-9685-ff5bb260df2e  (Balanced)
     GUID Alias: SCHEME_BALANCED
     Subgroup GUID: 238c9fa8-0aad-41ed-83f4-97be242c8f20  (Sleep)
       GUID Alias: SUB_SLEEP
       Power Setting GUID: 9d7815a6-7ee4-497e-8888-515a05f02364  (Hibernate after)
         GUID Alias: HIBERNATEIDLE
         Minimum Possible Setting: 0x00000000
         Maximum Possible Setting: 0xffffffff
         Possible Settings increment: 0x00000001
         Possible Settings units: Seconds
       Current AC Power Setting Index: 0x00000000
       Current DC Power Setting Index: 0x00000000
   ```

   Expected: `Current DC Power Setting Index: 0x00000000` turns off the "Hibernate after" timer, and only the timer. Any other value is the number of seconds after which the sleep turns into hibernation, and the session ends there with the exit reason `Hibernate, or Shutdown` (Failure modes, row 4). With hibernation enabled (step 1 lists `Hibernate`), Windows can still hibernate from its own standby battery budget: my first night ended that way after 8.69 h with `HibernateTimeoutInSeconds` 0 in its session metadata (Field notes, 2026-11-06). The restricted-standby budget can also end an early session of the night, so the start of a night can be split into two or more sessions (step 5: the 1.48 h and 0.27 h lines). My 56.92 h single session ran with hibernation turned off on the laptop (Field notes, 2026-11-09); no step here changes that.

3. **Sleep on battery with the lid closed.** Unplug the charger, note the charge level, close the lid, and leave it closed for a whole night (6 h or more). With hibernation enabled, the sleep can end early in `Hibernate, or Shutdown` (step 2). Open the lid the next morning and log in. No command: SleepStudy records the session by itself.

   > [!NOTE] Short sessions
   > Microsoft's SleepStudy page says "overall" modern standby sessions under ten minutes are not tracked and that periods of more than one hour reflect real use. My reports listed a 23 s Sleep session anyway, with no residency data (step 5), and a 16 min one that read higher than the long sleeps. Use one long session per test.

4. **Generate the report** in an elevated PowerShell (Microsoft's SleepStudy page asks for an Administrator Command Prompt; I ran it elevated and did not try without). An elevated shell often starts in `C:\Windows\System32`: `cd` into the working folder first, so the report lands next to the scripts.

   ```console
   PS> powercfg /sleepstudy /duration 7 /output sleepstudy.html
   Sleep Study report saved to file path C:\Users\user\sleepstudy\sleepstudy.html.
   ```

   Expected: one line naming the saved file, about 3-5 MB on my laptop. `/duration 7` covers the last seven days (default three, at most 28). The report is a web page; its data is the JSON object `var LocalSprData` inside it, which the scripts read.

5. **Print residency and drain for each battery sleep.**

   ```console
   PS> python -I residency.py sleepstudy.html
   2026-11-06T00:43 1.48 h  HW 98.7%  SW 98.8%  drain 582 mWh = 392 mW = 0.58 %/h  austerity=0  exit: Restricted Standby Battery Drain Budget Exceeded
   2026-11-06T02:12 8.69 h  HW 99.8%  SW 99.8%  drain 2710 mWh = 312 mW = 0.46 %/h  austerity=1  exit: Hibernate, or Shutdown
   no residency data: 2026-11-07T03:47:52Z
   2026-11-07T03:48 0.27 h  HW 97.9%  SW 98.0%  drain 112 mWh = 421 mW = 0.62 %/h  austerity=0  exit: Restricted Standby Battery Drain Budget Exceeded
   2026-11-07T04:04 56.92 h  HW 99.3%  SW 99.9%  drain 19234 mWh = 338 mW = 0.50 %/h  austerity=1  exit: Lid
   2026-11-09T21:52 2.00 h  HW 98.1%  SW 98.1%  drain 574 mWh = 287 mW = 0.42 %/h  austerity=0  exit: Hibernate, or Shutdown
   ```

   Expected: one line per type-2 Sleep session on battery (`residency.py` keeps `Type` 2 with `OnAc` false), oldest first, and a `no residency data:` line for a session that carries no residency fields (Failure modes, row 3). Last night's session is the newest line. Here the 2026-11-07 04:04 line is a 56.92 h sleep: HW low-power 99.3 %, 19234 mWh used, 338 mW on average, 0.50 % of a full charge per hour.

   How to read one line:
   - Time: the session start, local time, to the minute.
   - `HW`: `Info.HwLowPowerStateTime` as a share of the session. This is the hardware DRIPS figure, the SoC in S0ix; it is the number that answers "did it reach S0ix". `SW` is the software DRIPS share (`Info.SwLowPowerStateTime`); Microsoft's page calls it the DRIPS rate without defining it further. On my sessions SW was never below HW (largest gap 0.6 points). Both fields are in 100 ns units, `Duration` is in microseconds; the script stops with `unit check failed` if HW time comes out more than 1 % longer than the session. The report's own page computes its `HW:` percentage from the same two fields and units, rounded to a whole percent.
   - `drain`: `EntryRemainingCapacity` minus `ExitRemainingCapacity`, in mWh, the battery's own reading at entry and exit. `mW` is that energy over the session hours, `%/h` is the same against the full-charge capacity at entry. Microsoft computes its "change rate" the same way: energy change divided by duration.
   - `austerity`: `Info.AusterityMode` from the session metadata. On my laptop it was 1 for a session that started right after a `Restricted Standby Battery Drain Budget Exceeded` exit, 0 otherwise; Microsoft's pages I read do not define it.
   - `exit`: why the session ended. `Lid` is the lid opening; `Restricted Standby Battery Drain Budget Exceeded` and `Hibernate, or Shutdown` are Windows' own exits (Microsoft lists the names, without explaining the budgets).

6. **Grade the session against Microsoft's colour bands.** Microsoft's SleepStudy page grades a Sleep session of 2 minutes or more as: red when "the DRIPS rate is <80% or the drain rate is >= 1% per hour", yellow when "the DRIPS rate is between 80-94% or the drain rate is between .33-1% per hour", green in all other cases. The same section also says a Sleep session "is colored Grey, since there is no information to determine if it's a good or bad session based on drain or time spent in DRIPS"; I grade by the thresholds. The page does not say whether the DRIPS rate here is the software or the hardware figure; on my sessions they differ by at most 0.6 points, so the band does not change. My 56.92 h session: DRIPS 99.3 % is in the green range, and 0.50 %/h is in the yellow drain band, so the session as a whole falls in yellow.

7. **(If HW is low) find what kept the SoC awake.**

   ```console
   PS> python -I blockers.py sleepstudy.html
   type,onac -> count, hours
     (0, False): 25, 4.16 h
     (0, True): 29, 22.03 h
     (1, False): 25, 0.57 h
     (1, True): 14, 25.03 h
     (2, False): 6, 69.37 h
     (2, True): 1, 0.12 h
   …
   screen-off sessions >= 10 min:
   …
   - 2026-11-05T19:43 type=1 ac=True 3.58 h drain=0 mWh Video Idle Timeout -> Input Keyboard
        99%    214.5 min  [PDC Phases] No CS Phase
         0%      0.4 min  [Audio Activity] Audio Active
         0%      0.0 min  [Audio Activity] Audio Streams
   …
   - 2026-11-07T04:04 type=2 ac=False 56.92 h drain=19234 mWh Transition To Sleep -> Lid
         0%      3.6 min  [FX Devices] PCI Express Root Port (\_SB.PC00.RP11)
         0%      2.4 min  [SOC Subsystems] NoHwDrips.C10
         0%      0.5 min  [Processors] CPU C0 Time.Non-attributed Time
         0%      0.2 min  [FX Devices] Intel(R) Wi-Fi 7 BE201 320MHz (\_SB.PC00.CNVW)
         0%      0.1 min  [PDC Phases] Low Power Phase
         0%      0.1 min  [FX Devices] USB xHCI Compliant Host Controller (\_SB.PC00.XHCI)
   - 2026-11-09T15:09 type=1 ac=True 6.71 h drain=0 mWh Video Idle Timeout -> Lid
        99%    402.7 min  [PDC Phases] No CS Phase
   - 2026-11-09T21:52 type=2 ac=False 2.00 h drain=574 mWh Transition To Sleep -> Hibernate, or Shutdown
         1%      1.6 min  [FX Devices] Intel(R) Wi-Fi 7 BE201 320MHz (\_SB.PC00.CNVW)
         0%      0.9 min  [FX Devices] PCI Express Root Port (\_SB.PC00.RP11)
         0%      0.4 min  [Activators] TCPIP
         0%      0.1 min  [PDC Phases] Low Power Phase
         0%      0.1 min  [SOC Subsystems] NoHwDrips.C10
   ```

   Expected: the first table counts sessions by `(type, on AC)`; type 2 is the Sleep session that `residency.py` reads, and `(2, False)` the ones on battery. All six of mine started with the lid closed (`EventLog.LidState` `Closed` in the session metadata); the script does not check the lid. The list below it prints, for every session of 10 minutes or more, the top six blockers with the share and minutes they were active. Its header says "screen-off sessions", but the list holds type-2 sleeps too: read the `type=` field of each entry. On a healthy sleep every blocker is a fraction of a minute to a few minutes: in the 56.92 h session the largest was the PCIe root port `RP11` at 3.6 min. A blocker near 100 % in a type-2 session is the thing to fix. The two type-1 lines are display-off time on AC, not sleep; see Failure modes, rows 1 and 2.

   > [!GOTCHA] Read type 2, not type 1
   > Type-1 sessions are screen-off periods, not sleep. On battery they last seconds to minutes and carry no residency figure; on AC the long ones (10 of the 14 in my 2026-11-10 report, each over 30 minutes) are display-off time in which the laptop never entered standby, and show `No CS Phase` at 99 %. None of them has a hardware residency field. Counting them gave me "HW DRIPS = 0, never reaches S0ix" on the first night, which was wrong.

## Verify

- The number of `(2, False)` sessions in step 7's table matches the number of lines step 5 printed, `no residency data:` lines included: 6 and 6 on my 2026-11-10 report (`(2, True)`, one 0.12 h sleep on AC, is skipped by `residency.py` on purpose: no drain on AC).
- Re-running `residency.py` on an older saved report prints the same lines for the sessions both reports cover. My 2026-11-06 report and the 2026-11-10 report give identical lines for the 2026-11-06 sleeps:

```console
PS> python -I residency.py sleepstudy-2026-11-06.html
2026-11-06T00:43 1.48 h  HW 98.7%  SW 98.8%  drain 582 mWh = 392 mW = 0.58 %/h  austerity=0  exit: Restricted Standby Battery Drain Budget Exceeded
2026-11-06T02:12 8.69 h  HW 99.8%  SW 99.8%  drain 2710 mWh = 312 mW = 0.46 %/h  austerity=1  exit: Hibernate, or Shutdown
```

## Undo / rollback

- Nothing to undo: no step changes a setting. Step 4 wrote one HTML file, and the first script run wrote Python's byte-code cache (`__pycache__\blockers.cpython-312.pyc`) next to the scripts; delete the working folder when you are done.
- If you changed the battery "Hibernate after" value for the test (step 2), set it back to the value step 2 printed before the change. On my laptop the hibernation settings changed outside this procedure: hibernation was off during night 2 (`powercfg /h off`, 2026-11-06), was on again from 2026-11-09 with a 7200 s battery timeout (Failure modes, row 4), and step 2 read 0 on 2026-11-10.

## Failure modes

| Symptom (exact message or output) | Cause | Fix |
|---|---|---|
| My own reading, not a message: a count over screen-off sessions gives "HW DRIPS = 0" | The count used type-1 sessions (screen-off fragments of seconds to minutes on battery, display-off time on AC); real sleeps are type 2 | Read only type-2 sessions: `residency.py` filters for them |
| `[PDC Phases] No CS Phase` at 99 % in a long screen-off session with `drain=0 mWh` | Type 1 on AC: the display was off, the laptop never entered standby | Run the test on battery with the lid closed |
| `no residency data: 2026-11-07T03:47:52Z` | A 23 s lid-closed sleep; that session had no `Info.HwLowPowerStateTime` field (25 metadata values instead of 75 or more) | None needed; use the long session. The `Z` on that time is not UTC, see Notes |
| `exit: Hibernate, or Shutdown` after 2.00 h | Battery "Hibernate after" was 7200 s that day; the session's own metadata holds `Settings.Hibernate.HibernateTimeoutInSeconds: 7200` | Check step 2 before the night; the session ends at that time |
| `ModuleNotFoundError: No module named 'blockers'` | `residency.py` was saved without `blockers.py` next to it | Save both scripts in one folder |

## Field notes

- **2026-11-10, Zenbook UX3405CA, BIOS 316, Windows 11 Pro 26200.9445, Python 3.12.10:** generated a fresh 7-day report (step 4) and ran both scripts on it and on the two saved nightly reports. Every line for a session that two reports share was identical across reports, and identical to the outputs I saved when each night was first read. The fresh report added one sleep, 2026-11-09 21:52: 2.00 h, HW 98.1 %, 287 mW = 0.42 %/h, ended `Hibernate, or Shutdown`: the battery "Hibernate after" value was 7200 s on that day, and it read 0 on 2026-11-10 (step 2). Saving `residency.py` alone in an empty folder reproduced the `ModuleNotFoundError`. The report's timestamp fields named `...Local` end in `Z` but hold local time: `EntryTimestamp` 2026-11-05T22:43:06Z and `EntryTimestampLocal` 2026-11-06T00:43:06Z for the same session, and the report-level `ReportInformation.UtcOffset` is 120 (minutes).
- **2026-11-09, same laptop, BIOS 316, Windows 11 Pro 26200.9445:** night 2, after a 23 s sleep and a 0.27 h one ended by the restricted-standby budget, one session from 2026-11-07 04:04 to 2026-11-09 13:00, 56.92 h, lid closed on battery, 57 % to about 29 % (39105 to 19871 mWh of 68016 mWh): HW 99.3 %, 338 mW = 0.50 %/h, exit `Lid`. Hibernation was off on the laptop for the whole session, so Windows' standby budget tripped but could not hibernate. In the battery sleep sessions no blocker passed 0.08 h. The large one in the report, `No CS Phase` 18.38 h, was screen-off time that never became sleep, all but 13.4 min of it on AC (the 13.4 min were one type-1 session on battery, 2026-11-06 11:33).
- **2026-11-06, same laptop, BIOS 316, Windows 11 Pro 26200.9445:** night 1. My first read counted only type-1 sessions and concluded the laptop never reached S0ix. The battery ones were seconds to minutes long and the AC ones were display-off time, neither of them sleep; the two type-2 sessions (step 5's first two lines) were the real sleeps. The 8.69 h one reached HW 99.8 %, 2710 mWh = 312 mW = 0.46 %/h of 68032 mWh. Its exit chain was Windows policy: the restricted-standby budget (`RSBudgetPercentage` 1 per `RSBudgetRefreshInterval` 10800 s in the session metadata) ended the first session at 02:12, the next ran with `AusterityMode` 1, and the standby budget (`StandbyBudgetPercentage` 5 per 43200 s) hibernated it at 10:53. Largest blockers in that session: PCIe root port `RP11` 0.6 min, `NoHwDrips.C10` 0.4 min, the BE201 Wi-Fi 0.1 min.

## Notes

- Timestamps: `residency.py` prints the first 16 characters of `EntryTimestampLocal`, so the times it prints are local; the `no residency data:` line prints the whole field, `Z` included, which still means local time here.
- The HTML itself shows the same figures in its session table ("% LOW POWER STATE TIME" with `HW:`, "ENERGY CHANGE", "CHANGE RATE"); the scripts exist to get them as text for many sessions and to skip the screen-off rows. The page computes its `HW:` and `SW:` percentages from the same fields as `residency.py`, rounded to whole percent.
- `blockers.py` adds up blocker `ActiveTime` over sessions; Microsoft's pages do not define that field. I read it as time a component was active (outside DRIPS) in the session, from the values in my reports.
- For a Windows-vs-Linux comparison, compare drain over sessions of similar length and at a similar charge level. My five battery sleeps read 421 mW (0.27 h), 392 mW (1.48 h), 287 mW (2.00 h), 312 mW (8.69 h) and 338 mW (56.92 h).

## Sources

- [Modern standby SleepStudy](https://learn.microsoft.com/en-us/windows-hardware/design/device-experiences/modern-standby-sleepstudy) (Microsoft Learn): running the report as Administrator, `/duration` up to 28 days, the column definitions (ENERGY CHANGE, CHANGE RATE, % LOW POWER STATE TIME with `HW:` on Intel and Qualcomm SoCs), the ten-minute and one-hour session notes, the colour bands quoted in step 6, the exit-reason names.
- [Powercfg command-line options](https://learn.microsoft.com/en-us/windows-hardware/design/device-experiences/powercfg-command-line-options) (Microsoft Learn): `/sleepstudy` with `/output` and `/duration`, `/availablesleepstates` (`/a`), `/query` (`/q`).
- [Modern Standby vs S3](https://learn.microsoft.com/en-us/windows-hardware/design/device-experiences/modern-standby-vs-s3) (Microsoft Learn): the system stays in S0 in Modern Standby; Screen Off and Sleep states since Windows 2004.
- [Prepare software for modern standby](https://learn.microsoft.com/en-us/windows-hardware/design/device-experiences/prepare-software-for-modern-standby) (Microsoft Learn): DRIPS as the platform's lowest power state.

## Appendix: the two scripts

`residency.py`:

```python
"""HW/SW low-power residency + drain for sleep (type 2) sessions on battery.
usage: python -I residency.py <html>
Hw/SwLowPowerStateTime unit = 100 ns (checked against Duration in us)."""
import sys, os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from blockers import load
d = load(sys.argv[1])
for x in d["ScenarioInstances"]:
    if x["Type"] != 2 or x["OnAc"]:
        continue
    vals = {v["Key"]: v["Value"] for v in (x.get("Metadata") or {}).get("Values", [])}
    if "Info.HwLowPowerStateTime" not in vals:
        print("no residency data:", x["EntryTimestampLocal"]); continue
    dur_s = x["Duration"] / 1e6
    hw = float(vals["Info.HwLowPowerStateTime"]) / 1e7
    sw = float(vals["Info.SwLowPowerStateTime"]) / 1e7
    if not (0 <= hw <= dur_s * 1.01):
        raise SystemExit(f"unit check failed: hw {hw} s vs dur {dur_s} s")
    dwh = x["EntryRemainingCapacity"] - x["ExitRemainingCapacity"]
    fcc = x["EntryFullChargeCapacity"]
    mw = dwh / (dur_s / 3600)
    print(f"{x['EntryTimestampLocal'][:16]} {dur_s/3600:.2f} h  HW {100*hw/dur_s:.1f}%  SW {100*sw/dur_s:.1f}%  "
          f"drain {dwh} mWh = {mw:.0f} mW = {100*mw/fcc:.2f} %/h  austerity={vals.get('Info.AusterityMode')}  "
          f"exit: {x['ExitReason']}")
```

`blockers.py`:

```python
"""Dump sleepstudy blocker groups for screen-off (standby) sessions.

usage: python -I blockers.py <sleepstudy.html>
"""
import json
import sys
from collections import defaultdict


def load(path):
    h = open(path, encoding="utf-8", errors="replace").read()
    key = "var LocalSprData = "
    i = h.find(key)
    if i < 0:
        raise SystemExit("LocalSprData not found")
    s = i + len(key)
    depth = 0
    for j in range(s, len(h)):
        c = h[j]
        if c == "{":
            depth += 1
        elif c == "}":
            depth -= 1
            if depth == 0:
                return json.loads(h[s:j + 1])
    raise SystemExit("LocalSprData not found")


def main():
    d = load(sys.argv[1])
    si = d["ScenarioInstances"]
    types = defaultdict(lambda: [0, 0.0])
    for x in si:
        t = types[(x["Type"], x["OnAc"])]
        t[0] += 1
        t[1] += x["Duration"] / 1e6 / 3600
    print("type,onac -> count, hours")
    for k, v in sorted(types.items()):
        print(f"  {k}: {v[0]}, {v[1]:.2f} h")

    # metadata friendly names per type (first instance)
    seen = set()
    for x in si:
        if x["Type"] in seen:
            continue
        seen.add(x["Type"])
        md = x.get("Metadata") or {}
        print(f"type {x['Type']}: {md.get('FriendlyName')}")

    # blocker totals over every session, by group/name
    agg = defaultdict(float)
    groups = defaultdict(set)
    for x in si:
        for g in x.get("BlockerGroups", []):
            for b in g.get("Blockers", []):
                agg[(g["Name"], b.get("Name", ""))] += b.get("ActiveTime", 0)
                groups[g["Name"]].add(b.get("Name", ""))
    print("\nblocker groups:", sorted(groups))
    print("\ntop blockers by active time:")
    for (g, n), v in sorted(agg.items(), key=lambda t: -t[1])[:40]:
        print(f"  {v / 1e6 / 3600:8.3f} h  [{g}] {n}")

    # per long screen-off session: top 6 blockers
    print("\nscreen-off sessions >= 10 min:")
    for x in si:
        hrs = x["Duration"] / 1e6 / 3600
        if hrs < 1 / 6 or not x.get("BlockerGroups"):
            continue
        bl = []
        for g in x["BlockerGroups"]:
            for b in g.get("Blockers", []):
                bl.append((b.get("ActiveTimePercent", 0), b.get("ActiveTime", 0), g["Name"], b.get("Name", "")))
        bl.sort(key=lambda t: -t[1])
        dwh = x["EntryRemainingCapacity"] - x["ExitRemainingCapacity"]
        print(f"- {x['EntryTimestampLocal'][:16]} type={x['Type']} ac={x['OnAc']} {hrs:.2f} h drain={dwh} mWh "
              f"{x['EnterReason']} -> {x['ExitReason']}")
        for p, at, g, n in bl[:6]:
            print(f"    {p:>3}%  {at / 1e6 / 60:7.1f} min  [{g}] {n}")


if __name__ == "__main__":
    main()
```

