Skip to content

Prefer kernel charge registers for battery percentage - #10076

Open
ludagoo wants to merge 6 commits into
omacom:quattrofrom
ludagoo:fix/battery-sysfs-percentage
Open

ludagoo wants to merge 6 commits into
omacom:quattrofrom
ludagoo:fix/battery-sysfs-percentage

Conversation

@ludagoo

@ludagoo ludagoo commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

What

UPower can latch energy-full after a Smart Battery blip (MacBook Air: 484 Wh on a 54 Wh pack; the MacBookPro13,3 in the thread: 733 Wh on 77 Wh) and keep it until the daemon restarts. The bar then shows a tenth of the charge, the panel a pack size in the hundreds of Wh and a time to full in days, and the low-battery warning fires on a full pack.

Reworked after review into a guard rather than a replacement:

  • omarchy-battery-status reads this battery's own sysfs registers for energy, full charge, percentage and size only when UPower's energy-full is more than twice energy-full-design, and prints a source kernel line to say so. Its existing time formula then counts to the real full charge. On every other machine the output is unchanged.
  • The battery service runs the script once at startup, again when the display device's energyCapacity moves by more than a factor of two (a latch appearing or clearing), and while a latch holds, as UPower's percentage moves a tenth of a point. No timer, and shell-idle-spawn-test.sh holds a sound machine to the one run. Its kernelPercentage is -1 unless the script named the kernel, so aggregation across packs, charge state and threshold detection stay with UPower.
  • The power panel reads that service property for the bar icon, fraction and label instead of polling on its own.
  • The eight-hour clause in chargeThresholdActive() is left alone; Keep charging visible with long time-to-full estimates #14141 owns that symptom.

Test plan

  • bash test/shell.d/battery-status-test.sh (new: latched charge- and energy-counting packs, a latch with no registers, a plausible energy-full; the latched assertions fail on quattro)
  • bash test/shell.d/battery-test.sh
  • bash test/shell.d/power-test.sh
  • ./test/cli
  • ./test/shell
  • MacBookAir7,2 against this head

Related #9668

@omarchybot omarchybot added bug Something isn't working compatibility Make Omarchy work better on everything! labels Sep 27, 2026
@omarchybot

Copy link
Copy Markdown
Collaborator

Reviewed at 92f86b0. Claude Opus 5.5 reviewed it, and Codex Medium gave a second opinion. The tests ran on a disposable Omarchy VM.

What ran: battery-status-test.sh, battery-test.sh, power-test.sh and ./test/cli all pass on this head. The new MacBook fixture fails on quattro (percentage 10%, size 484Wh), passes on this head (99%, 52Wh), and fails again with bin/omarchy-battery-status reverted. That fixture is a stubbed upower, so it proves the override wins, not that it fixes the latch on real hardware. A VM has no Smart Battery to reproduce that.

A regression on machines with two packs. bin/omarchy-battery-status reads only the first BAT that upower -e lists. Until now the bar and the low-battery service went by UPower.displayDevice, which adds up every pack. With this change, BatteryModel.js:19 and power/Model.js prefer the script's percentage. I tried it on the VM with BAT0 at 2/20 Wh and BAT1 full at 23/23 Wh. On quattro, a displayDevice at 58% gives level 58, no warning. On this head, the same machine gives level 10, notify true, so a ThinkPad with a full second pack gets a low-battery warning and a 10% bar. Swap the packs and the warning comes too late instead.

Threshold detection is skipped when the script cannot see a hold. power/Model.js:67 returns false whenever the script says charging or fully-charged. But the script only says holding when it finds an online Mains supply and threshold metadata. Say firmware limits charge without exposing a threshold file, and the pack reports Full at 80%. That used to show as Threshold. It now shows as Fully charged. Codex raised this one, and I checked it against the source.

Smaller things in the script:

  • At line 87, UPower's charging decides which remaining-time formula is used, even when sysfs says Discharging.
  • Line 89 drops a negative charging current, which leaves time empty instead of keeping UPower's figure.
  • The energy_* branch (line 113) fixes percentage and size but keeps UPower's time-to-full, which is the figure the description says stretches to days.
  • Service.qml:66 keeps the last batteryInfo when a poll prints nothing. The low-battery check then keeps preferring a stale percentage over UPower.

Proportionality. The bug is one wrong denominator on one machine. This change hands percentage, size, state and time for every laptop to sysfs. It also moves both consumers off UPower and adds a 3-second poll that runs while the panel is closed. That poll is cheap (about 14 ms per run on the VM), but it adds up next to the service's own poll. A fix kept to the latched energy-full would carry far less risk. For example, use sysfs only when UPower's energy-full is implausibly far above energy-full-design, and leave aggregation and state alone.

Also: the branch conflicts with quattro in shell/plugins/services/battery/Service.qml, which has since gained the power-profile polling. There is no open issue for this bug. Because the problem depends on the hardware, it also needs confirmation on an affected machine from a maintainer or another reporter, against the full head SHA.

Nothing was pushed. This is waiting on the author: the two-pack regression and the conflict need resolving, and ideally the change should be narrowed to the latched value.

@andersryanc

Copy link
Copy Markdown

Confirming this on a MacBookPro13,3 (15" 2016, Omarchy 4.0.4, upower 1.91.3, battery SMP bq20z451 via ACPI SBS).

After boot UPower had latched energy-full: 733.4 Wh (design 76.6 Wh). Sysfs was fine: charge_full=4806000, charge_now=2693000, capacity=41. UPower then reported 4.2% and 21.7 h to full. Since that's over the 8 h cutoff in chargeThresholdActive(), the power panel showed "Battery state: Holding" while the battery was actually charging at ~32 W.

sudo systemctl restart upower re-read the pack (55.2 Wh, 65%, 38 min to full), so UPower is reading the full-capacity value once at startup and keeping it. Reading charge_now/charge_full from sysfs as this PR does would avoid it. +1 for merging.

@omarchybot

Copy link
Copy Markdown
Collaborator

Re-reviewed at 92f86b0 after @andersryanc's report. The head has not moved, so this covers only what that report changes. Claude Opus 5.5 read it against the source, and Codex Medium gave a second opinion. Codex agreed with everything below, though its independence is not guaranteed. Nothing new was run on a worker. The tests from the last review still stand at this head.

The bug is real on a second machine. A MacBookPro13,3 latched energy-full at 733 Wh against a 76.6 Wh design, which is the same failure this PR describes. Open issue #9668 reports it on a third machine, at 654 Wh against 45.8 Wh. I have linked #9668 here, because this PR fixes the symptom it reports.

The "Holding" state comes from the eight-hour rule. On quattro, chargeThresholdActive() in shell/plugins/panels/power/Model.js treats a time to full of eight hours or more as a charge-limit hold. With a latched denominator, UPower estimated 21.7 hours while the pack was charging at 32 W, so the panel showed Holding. This PR drops that clause, and power-test.sh line 29 covers the change: it passes on this head and fails on quattro. #14141 makes the same one-line change on its own and also fixes #9668. Because it carries no other changes, it does not bring the two-pack regression from the last review. If the maintainer wants the Holding symptom fixed before this PR is ready, #14141 does it.

The report favours the narrower fix. Both latched readings are 9 to 14 times the design capacity, so a guard that uses sysfs only when UPower's energy-full is implausibly far above energy-full-design would catch both. It would also leave aggregation and charge state alone on every other laptop.

Nothing else has changed. The two-pack regression, the threshold detection that is skipped, the smaller defects in bin/omarchy-battery-status and the conflict with quattro in Service.qml all still stand. The report confirms the bug, but nobody has yet tested this head on an affected machine. That test is most useful once the branch is updated, so it can be done against the head that will actually be merged.

This is waiting on the author to resolve the regression and the conflict, and ideally to narrow the change to the latched value.

@ludagoo

ludagoo commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Reworked after the review, rebased on current quattro (3c7027d) and force-pushed.

  • Narrowed to the latched value. omarchy-battery-status reads sysfs only when UPower's energy-full is more than twice energy-full-design (both confirmed cases are 9 to 14 times), and prints source kernel when it did. Its existing time formula then counts to the real full charge. On every other machine the output is unchanged.
  • Two-pack regression gone. The shell prefers the script's percentage only when that line is present. Otherwise the bar, the panel and the low-battery check go by UPower.displayDevice as on quattro, so a second pack still aggregates. Charge state and threshold detection are no longer touched, which also drops the skipped-threshold case.
  • No polling. The 3-second panel timer is gone. The battery service runs the script once at startup, again when the display device's energyCapacity moves by more than a factor of two (a latch appearing or clearing), and while a latch holds, as UPower's percentage moves a tenth of a point. A sound machine runs it once, which shell-idle-spawn-test.sh now checks, with an allowance for a latched one. The panel reads the service's kernelPercentage instead of polling per monitor.
  • Eight-hour clause left as is; Keep charging visible with long time-to-full estimates #14141 owns that.
  • The smaller script defects fall away with the state and time overrides. The one that still applies is handled: UPower's time-to-full fallback is dropped while latched, since it counts to the latched value; time-to-empty stands.

The three battery suites, ./test/cli and ./test/shell pass here. The new latched assertions in battery-status-test.sh fail on quattro.

@andersryanc, if you get a chance, omarchy-battery-status --shell from this head on your MacBookPro13,3 should print source kernel and the sysfs percentage while UPower is latched. I will test the bar on my MacBookAir7,2 next.

@greptile-apps

greptile-apps Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The changes affect battery status across the shell’s warning and power-panel workflows.

The PR still needs the existing multiple-battery issue fixed before merging.

Findings

  1. P1 One pack replaces total charge ▶

Summary

Adds recovery when battery registers cannot be read and remembers refresh requests that arrive while the helper runs.

  • Battery status uses the battery’s own readings when UPower’s full-charge value jumps.
  • Battery warnings and the power panel use the corrected charge level.

Reviews (7) · Last reviewed commit: "Fall back to the design capacity when la..." · Reviewed by Greptile

Comment thread shell/plugins/panels/power/Model.js Resolved
@omarchybot

Copy link
Copy Markdown
Collaborator

Re-reviewed at 11d2d3c, after the rework. Claude Opus 5.5 reviewed it, and Codex Medium and muse-spark-1.3 (thinking high) each gave a second opinion. Tests ran on a disposable Omarchy VM.

The rework answers the last review. The two-pack regression, the skipped threshold detection, the per-panel poll and the conflict with quattro are gone. A sound machine runs the status script once at startup and is otherwise unchanged.

What ran: battery-status-test.sh, battery-test.sh, power-test.sh, shell-idle-spawn-test.sh and ./test/cli all pass at this head. The new latched fixture fails with quattro's script (percentage 10%, size 484Wh) and passes with this one (99%, 52Wh). The full ./test/shell has five failures (ascii, branding-about-animation, lock-appear-budget, omarchy-kernel, runtime-smoke), and all five fail the same way on quattro on the same VM. I also checked on the VM that the power panel's firstPartyServiceFor("omarchy.battery") resolves to the battery service, so the bar does get kernelPercentage.

One fix pushed (11d2d3c). The status script prints nothing when it finds no battery for a moment, which Panel.qml notes happens around AC plug events. If that happened while UPower was latched, the service set kernelPercentage to -1 but kept the latched capacity as its baseline, so shouldRereadKernel never asked again. The bar and the low-battery check then went back to UPower's latched tenth until the shell restarted. An empty run now keeps the last kernel percentage and clears the baseline, so UPower's next change runs the script again. Greptile raised this, all three reviewers reached it, and battery-test.sh covers the new kernelReading helper. The one QML line that wires it in is checked by reading, not by a test.

Still open, for the maintainer: with this PR alone, a latched MacBook charging below 99% still shows Threshold and Holding. That's because chargeThresholdActive() in power/Model.js still treats UPower's inflated time to full (21.7 hours in the MacBookPro13,3 report) as a hold, and that hides the corrected 45-minute estimate. #14141 removes that clause. Changing it here too would conflict with #14141, so the two belong together.

Noted, not changed: on a two-pack machine where one pack latches, the correction uses the first pack's percentage rather than an aggregate. That case hasn't been reported anywhere, and a sound two-pack machine is unchanged from quattro. Also, a reading taken while the latch clears stays in place until UPower's next change.

What it waits on: a VM has no Smart Battery, so nothing here reproduces the latch itself. The fixtures prove the override wins, not that it fixes real hardware. @andersryanc, if you can, please test this exact commit on the MacBookPro13,3 while UPower is latched. Check out 11d2d3c83e0640f2ab74be5e2cd2926288a7f6bf from ludagoo/omarchy (branch fix/battery-sysfs-percentage), run omarchy dev link <checkout>, and after the reboot omarchy-battery-status --shell should print source kernel and the sysfs percentage, and the bar should match it. omarchy dev unlink restores the packaged install. A maintainer confirming on an affected MacBook counts just the same.


function batteryPercentage(device, kernelLevel) {
if (!device || !device.isPresent) return -1
if (typeof kernelLevel === "number" && kernelLevel >= 0) return kernelLevel

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 One pack replaces total charge

kernelLevel comes from the first physical battery found by omarchy-battery-status, but this return replaces the whole machine's UPower.displayDevice percentage. On a two-battery laptop where that first pack has a latched reading, the bar and low-battery warning follow only that pack, ignoring the other pack's remaining charge. Combine readings across packs, or restrict this override to machines with one battery.

@omarchybot

Copy link
Copy Markdown
Collaborator

Re-reviewed at 4a12795, after the merge of quattro. Claude Opus 5.5 reviewed it, and Codex Medium and muse-wonder (thinking high) each gave a second opinion. Tests ran on a disposable Omarchy VM.

The merge is correct. The only conflict was in test/shell.d/battery-status-test.sh, and the resolution drops the rg check that quattro removed in #16334. Otherwise the PR's diff against quattro is the same as before. The kernel path also works with #16278's change to the AC-at-100% fallback: it replaces the percentage and pack size, and leaves the state and live draw that #16278 reads alone. Both reviewers agreed.

What ran: at the merged head, battery-status-test.sh, battery-test.sh, power-test.sh, shell-idle-spawn-test.sh and ./test/cli all pass, with the shell running from this branch. The latched fixture still fails with quattro's script (size 484Wh) and passes with this one. I ran those again after the fix below and they still pass. I didn't rerun the full ./test/shell.

One fix pushed (4e726a2, comment reworded in 4a12795). Greptile's second comment holds. The low-battery timer fired as soon as the shell started, before the status script answered. So on a latched MacBook running on battery below about 90%, UPower's latched tenth triggered the low-battery warning and the battery-low hook on every shell start, the false warning this PR exists to stop. The timer no longer fires on start. The startup read already runs checkBattery() once it has the correction, so the first check uses it. If the script never answers, a real low-battery warning comes at most one 30-second tick later. quattro shows the same warning, so this finishes the fix rather than undoing a regression. Both reviewers found no functional defect in it. They pointed out that a power-source change or an empty run can still check before the correction lands, so the comment now calls the timer a fallback rather than a guarantee.

For the maintainer: the two-pack case (Greptile's first comment). It holds as stated: the correction uses the first pack's percentage in place of UPower's total. On a two-pack laptop where the first pack latches and the packs are far apart, that can hide a low-battery warning that quattro would give. The reviewers split on it. Codex Medium would limit the override to machines with one battery. muse-wonder thinks that's out of proportion for a latch only seen on single-pack MacBooks. A sound machine, one or two packs, is unchanged from quattro. I've left it as is, since which way to go is your call.

Still waiting on: a VM has no Smart Battery, so the latch itself has not been reproduced, only the override. @andersryanc, if you can, please test 4a12795a4a7b394b0b2ff59cb62bdecc626adc2a from ludagoo/omarchy (branch fix/battery-sysfs-percentage) on the MacBookPro13,3 while UPower is latched, as described in my last comment: omarchy dev link <checkout>, reboot, and omarchy-battery-status --shell should print source kernel with the sysfs percentage, and the bar should match it. A maintainer confirming on an affected MacBook counts just the same. #14141 is still needed alongside this for the Holding label while charging.

ludagoo and others added 5 commits November 4, 2026 07:21
UPower can latch energy-full on a garbage Smart Battery reading (a MacBook
Air: 484 Wh on a 54 Wh pack; a MacBook Pro: 733 Wh on 77 Wh) and keep it
until the daemon restarts. The bar then shows a tenth of the charge, the
panel a pack size in the hundreds of watt-hours and a time to full in
days, and the low-battery warning fires on a full pack.

A full charge beyond twice the design capacity cannot be real, so
omarchy-battery-status reads this battery's own sysfs registers for its
energy, full charge, percentage and size in that case, and names the
kernel as its source. The battery service runs the script once at
startup, again when UPower's full charge moves by more than a factor of
two, and while a latch holds, as UPower's percentage moves; its kernel
percentage stands in for the display device's in the low-battery check
and the power panel's bar icon. On every other machine nothing changes:
aggregation, state and time stay with UPower, and the script runs once.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the kernel figure

The status script prints nothing when it finds no battery for a moment, which happens around AC plug events. The service took that as a sound answer: the kernel percentage went to -1 while the latched capacity stayed recorded, so no later change ever asked again and the bar and low-battery check fell back to UPower's latched tenth until the shell restarted. An empty run now keeps the last kernel percentage and clears the recorded capacity, so UPower's next change runs the script again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex Medium <noreply@openai.com>
The warning timer fired on start, before the status script answered, so a latched MacBook on battery checked UPower's tenth of the real charge and ran the low-battery warning and battery-low hook at every shell start. The startup read already calls checkBattery() when it finishes; the timer now only repeats, so a genuine warning waits at most one 30-second tick if the read never answers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: muse-wonder thinking high <noreply@muse.dev>
A power-source change, an empty status run or a helper that never answers can still check before the kernel correction lands; the comment no longer claims otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex Medium <noreply@openai.com>
Co-Authored-By: muse-wonder thinking high <noreply@muse.dev>
@ludagoo
ludagoo force-pushed the fix/battery-sysfs-percentage branch from 4a12795 to 5793ac4 Compare November 4, 2026 10:25
…, and rerun a status read a change overtook

Past a latched energy-full, a register pair that doesn't answer used to
leave no source line, so the shell took -1 (UPower's latched figure) and
never read again. Use UPower's energy over the design capacity there
(source design); with no energy either, print source unavailable, which
the shell treats as no reading: it keeps the last level and retries.
A UPower change that arrives while the status script runs now reruns it
when the run exits instead of being dropped.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working compatibility Make Omarchy work better on everything!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants