BatteryControl 1.3.1
BatteryControl 1.3.1
Bug-fix release. Every fix below was found by operating the app end-to-end on real hardware (M3 MacBook Air, macOS 15.8) and fixed at the root cause, with regression tests. No new features.
Fixed
Calibration cancel left the charger undetected. Starting a calibration discharges the battery by cutting adapter input at the firmware level, so macOS temporarily reports the (physically attached) charger as absent — that part is expected. The bug: cancelling the calibration re-programmed your charge limit but never released that adapter cut, so the Mac kept running on battery with the charger plugged in until the battery happened to drift back down to the limit. Cancelling now releases the cut immediately, before the next control tick, on every ending path (cancel, safety abort, natural finish). Verified on hardware: after cancel, macOS reports AC power again within seconds and the adapter key reads back as released at the SMC level.
Out-of-range CLI/XPC values were silently clamped. batterycontrol charge start 101 used to be accepted and reported "Charging to 101%"; limit set 4 stored an invalid 4% policy before the backend rejected it. Invalid percentages are now rejected with a clear error at all three layers (CLI, XPC boundary, daemon) — nothing is transformed, nothing reaches the hardware.
Charging settings could go stale. If you changed the limit from the CLI while the app was open, the GUI kept showing its old sliders. Conversely, an unrelated background status refresh could clobber an edit you were mid-way through making. The settings pane now reloads only when the authoritative policy actually changes.
Recovered hardware could still show an old error. After a failed operation recovered and verification succeeded, a stale "last error" lingered in the UI. Successful verification now clears it (the raw diagnostic log keeps the history).
Legacy SMC backend could reassert the wrong action. On machines using the legacy charging keys, recovery reasserted "normal" instead of the active inhibit/discharge state, because the backend forgot what it had last applied. It now remembers.
Menu bar icon went generic after hide/show. Toggling the menu bar icon off and on could bring it back as a generic bolt instead of the current battery/control state. Icon and menu caches are now invalidated on show.
Internal: ControlAttemptResult ignored its verified initializer argument, so verification flags were always false in control-attempt records. Fixed with regression coverage.
Validation
- 292 tests, 0 failures (6 new regression tests)
- Debug and Release builds succeed with zero compiler warnings
- Physical run on M3 / macOS 15.8: limit changes, force-charge start/cancel, calibration start/cancel, charger recovery after cancel (adapter key reads
0), 80/78 firmware limit active and hardware-verified, ~3 minutes of stable observation with no micro-cycling - End-to-end: concurrency (15 parallel CLI status readers against live policy writes), daemon restart with policy persistence, GUI/CLI agreement
Checksums
See SHA256SUMS in the release assets. Verify with:
shasum -a 256 -c SHA256SUMS