Releases: Ednk-1312/BatteryControl
Release list
BatteryControl 1.3.4
BatteryControl 1.3.4
Development on BatteryControl has stopped. This release is the same source code as 1.3.3 —
the version bump and the status notice in the README are the only changes — and it's the
last planned release. The repo stays up and the project isn't going anywhere.
Why
BatteryControl was written because macOS had no way to hold a charge below 80%. macOS 26
now ships Apple's own Charge Limit (System Settings → Battery, 80–100%), and on the
author's Mac that covers what this app was for. The app has been uninstalled, and there's
no active development on it anymore. If someone needs support on it, that's the kind of
thing that could bring development back — but don't plan on it.
What 1.3.4 contains
- The status notice in the README, plus maintenance notes in SECURITY.md and
CONTRIBUTING.md. No code changes. - The version bump itself, so the last planned release is a real, labeled artifact.
An honest note about this build
The binaries were recompiled with Xcode 26.3 on macOS 26, and the full test suite passes
(308/308) with clean Debug and Release builds. But the app is no longer installed on the
author's machine, so nothing in this release was re-validated against real battery
hardware. What was physically tested is exactly what the README's compatibility section
has always said: one M3 MacBook Air (Mac15,13) on macOS 15.8, with write/readback
evidence recorded. Everything else remains capability-tested with per-write verification.
What still holds
- Security reports still go through
private vulnerability reporting
and will be read. With no planned releases, a fix can't be promised — but a serious
report is also the most likely thing to bring the project back for one more fix. - Issues and pull requests may sit unanswered.
- Forks are welcome (MIT). If you take it further, keep the rules in CONTRIBUTING.md:
runtime capability detection, readback verification on every write, and honest state
reporting. They're what keep an SMC-writing tool from lying to you.
If you're using it
- On macOS 26.4+, Apple's built-in Charge Limit is the supported way to cap at 80–100%.
- Below 80%, this release still works as documented on hardware with a recognized
firmware signature — with the same caveats the compatibility section has always stated.
Read it before trusting any build with your battery.
Downloads
| File | Contents |
|---|---|
BatteryControl-1.3.4.pkg |
App + CLI + embedded daemon |
BatteryControlCLI-1.3.4.pkg |
Just the CLI, no daemon |
BatteryControl-1.3.4.zip |
The app + CLI as a zip |
SHA256SUMS |
Hashes of all three (shasum -a 256 -c SHA256SUMS) |
The installers are unsigned and not notarized; Gatekeeper may ask you to approve the app
on first launch. Only download from the
releases page of this repository —
check SHA256SUMS if you want certainty about what you got.
BatteryControl 1.3.3
Navigation bug-fix release, hot on the heels of 1.3.2.
Fixed
The sidebar could trap you in a pane. The sidebar used a selection-bound list; on macOS 15 that selection machinery could desync from the app's navigation state after entering a pane (Charging was the reported case), and from then on sidebar clicks stopped registering — the only escape was relaunching the app. Sidebar rows are now plain buttons that set the navigation state directly, so a click is a stateless action and navigation structurally cannot wedge. The selected row is still visibly marked.
Reopen events no longer yank you back to the Dashboard. Showing the window (from the menu bar, or relaunching the app) used to reset whatever pane you were working in. Navigation is now preserved.
The menu-bar "Custom…" item now actually opens Charging. It set the Charging pane and then had it instantly overwritten back to Dashboard before the window appeared.
Validation
308 tests passing, zero compiler warnings, Debug and Release builds clean. Physically verified on the development M3 MacBook Air: navigation switches cleanly between all seven panes, menu-bar and reopen paths preserve the current pane, and SMC readback confirms the 80/78 limit remains active and enforced. Hardware support remains capability-based; only the author's configuration is physically validated.
SHA256SUMS is attached; verify your download against it. Builds are development-signed, so macOS Gatekeeper will ask you to approve the app on first launch.
BatteryControl 1.3.2
Bug-fix and safety-hardening release. Every fix was found by operating the app end-to-end on real hardware (M3 MacBook Air, macOS 15.8) or by an external review of the CLI, and fixed at the root with regression tests. No new features.
Fixed
Duplicate app instances are now impossible. Two copies of BatteryControl could run at once (two menu-bar icons, doubled polling). A single-instance guard now hands off to the running app and exits, including the relaunch-after-update path.
batterycontrol limit off now requires --confirm when a limit is active. A cron job, stale script, or agent could previously silently remove a deliberate charge limit. Without the flag the command is refused (exit 6) and nothing changes; with no limit active it remains a harmless no-op. limit set, status commands, and bounded sessions like discharge are unchanged.
The menu bar no longer lies about platform support. It reported "Active & verified" on unsupported Macs because it hardcoded the supported flag; it now shows the same honest state as the dashboard.
The dead "Custom" preset button is gone. Clicking it did nothing; the custom controls live directly below where it was.
Discharge consent can no longer leak between sessions. A cancelled safety-floor dialog left the floor-removal toggle stuck on for the next session's UI. The consent state now resets correctly (pure, unit-tested decision).
Honest banner when the app was updated while running. A GUI process opened before an upgrade showed a contradictory "helper required" banner while the dashboard reported an active, verified limit. Stale processes now show a "reopen the app" banner, real version mismatches show update-the-app guidance, and the two states can't contradict each other anymore.
Banner copy matches real verification timing. The "not verified" banner now reflects the actual ~2-minute firmware taper window instead of implying failure sooner.
Safety model
The CLI now documents and enforces a pinned classification: read-only commands (status, diagnostics, compatibility) stay frictionless for scripts and agents; persistent policy removal (limit off, uninstall) and below-floor discharge require explicit flags; bounded, self-terminating sessions and safe-direction changes stay ungated.
Validation
308 tests passing, zero compiler warnings, Debug and Release builds clean. Physically verified on the development M3 MacBook Air: duplicate-launch guard, limit gate refusal + confirm + restore, SMC readback 80/78 active. Hardware support remains capability-based; only the author's configuration is physically validated.
SHA256SUMS is attached; verify your download against it. Note: builds are ad-hoc/development signed, so macOS Gatekeeper will ask you to approve the app on first launch.
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
BatteryControl 1.3.0
BatteryControl 1.3.0
Daily-use polish, a first-class uninstall, and hardening from the pre-beta audit.
Uninstall, done properly
Settings → Uninstall BatteryControl… now does the whole job. The daemon first restores normal charging and verifies it, then removes the privileged helper, the LaunchDaemon plist, root-owned state, and the CLI. Success is only reported after the helper binary, the plist, and the loaded launch job are all confirmed gone — files disappearing isn't enough, since launchd can keep a job loaded after its plist is deleted. You choose whether to keep or remove local settings and history. The CLI gets the same path: batterycontrol uninstall --confirm (add --remove-data to wipe local data; without --confirm it refuses to do anything).
Real setup guide
"Open Setup Guide" on the helper banner used to do nothing but re-select the dashboard. It now opens an actual guide: what the helper is for, the three install steps, live helper status, and the Install Helper button. No more guessing.
Chronically Plugged In preset (50%)
A new preset for Macs that spend most of their time on power: 50% upper limit, 48% lower. The battery spends far less time at high charge and voltage, which can slow long-term wear. The preset row explains the reasoning honestly — it's not a guarantee, and occasional full charging still helps the gauge stay accurate.
Less noise below the floor
The "safety floor removed" warning on the discharge panel used to show the moment you flipped the switch, even with the target safely above 20%. Now the warning appears only when the discharge target is actually below 20%, and during a session only when the battery itself drops below 20%. The red switch and its confirmation dialog are unchanged.
Hardening (from the audit)
- All privileged install/uninstall shell commands now validate the app path and quote arguments, so an unusual install location can't become command injection.
- The daemon's XPC authorization no longer accepts any app signed by the same team — clients must also carry an approved code identifier (
com.batterycontrol.appor the standalone CLI). - Local history writes are serialized, so clearing history or uninstalling can't be raced by an older queued write that resurrects deleted data.
- Support bundles now include the last daemon-confirmed status snapshot (policy, override, telemetry, verification) so a report actually shows what BatteryControl believed it was doing, and event details get path-shaped values redacted before export.
Validation status
285 tests passing, zero compiler warnings, Release build clean, package contents and SHA256SUMS verified. Physical hardware validation remains what it has always been: the M3 MacBook Air (Mac15,13, mBoot-20457.1.29, macOS 15.8) profile is the one physically verified configuration. Everything else is capability-based. The uninstall flow's privileged removal steps were code-reviewed and unit-covered in this release but full physical uninstall validation is still pending — worth doing on a test machine before relying on it.
Full changes since 1.2.0: see commit 6241ffa.
BatteryControl 1.2.0
BatteryControl 1.2.0
This release finishes the daily-use controls work without changing the privileged control boundary.
What changed
- Added a clearer Battery Health section with charge state, cycle count, temperature, design capacity, full-charge capacity, and a labeled capacity-ratio estimate when telemetry is available.
- Added named charge presets: Daily (80/70), Battery Saver (70/60), Full Charge, and Custom.
- Added menu-bar quick actions for presets and temporary charging overrides.
- Temporary charge overrides are now daemon-owned and persisted atomically. They can run until the target is reached, be cancelled manually, or use a one-hour/two-hour deadline. Expired overrides restore the saved policy after a daemon restart instead of being silently extended.
- Added bounded daemon-owned event history for meaningful policy, override, verification, discharge, and capability transitions. Repeated ticks are not recorded as separate events.
- Added sanitized support-bundle export containing compatibility information and recent factual events. No upload or telemetry was added.
- Fixed GUI snapshot deduplication so polling timestamps do not cause unnecessary SwiftUI updates.
- Added CLI support for
charge start ... --for 1h|2h. - Updated documentation and compatibility wording.
Safety and compatibility
All control operations still go through the privileged daemon and the existing XPC boundary. Hardware capability detection, readback verification, unknown-hardware read-only behavior, safety-floor consent, and recovery paths remain in place.
BatteryControl does not replace macOS Optimized Battery Charging. It provides user-defined policy where the detected backend can safely verify it.
Physical validation in this release was performed on an Apple M3 MacBook Air running macOS 15.8 with mBoot-20457.1.29. The 70/60 preset, restored 80/70 policy, Full Charge deactivation/restoration, temporary override, cancellation, persisted deadline, and daemon restart recovery were exercised there. Other Apple Silicon generations and macOS versions remain capability-detected or untested rather than physically claimed.
Release artifacts
BatteryControl-1.2.0.pkg— GUI, daemon, and CLIBatteryControlCLI-1.2.0.pkg— CLI-only packageBatteryControl-1.2.0.zip— app archive and CLISHA256SUMS— checksums for all artifacts
The installer is signed with the available Apple Development identity but is not claimed to be notarized. Verify downloaded files against SHA256SUMS.
1.1.1 — app icon, safety-floor dialog fix, floor explanation
BatteryControl 1.1.1
UI fixes and the first real app icon. No daemon logic changed — the safety
engine, verification, and compatibility behavior are identical to 1.1.0.
App icon
BatteryControl finally has a real icon: a macOS-style squircle, blue→teal
gradient, a battery showing an 80% fill with a charge-limit line, and a bolt.
While adding it I found why the app never had one: the app target's Resources
build phase was referenced in the project but never defined, so the asset
catalog was never compiled at all. That's fixed, and the compiled icon is
verified end-to-end (loaded from the built .app exactly the way the Dock
loads it). The generator lives at scripts/make-app-icon.swift, and the icon
now sits at the top of this README.
Safety-floor dialog wording
The confirmation dialog for removing the safety floor used to say
"Remove safety floor and allow discharge to 80%" — it concatenated the
current discharge-target value into the consent action. But the consent isn't
about the target; it's about the floor. The button now says exactly
"Remove Safety Floor", matching the switch that triggers it.
"What happens at the floor" explained
The Discharge tab has a new About the safety floor section documenting
what actually happens when a discharge reaches the floor:
- The floor is a stop line, not a wall. The daemon checks the battery every
20 seconds while a discharge runs and keeps discharging only while the
charge is above the floor. - The moment a check reads the floor value or lower, the session ends by
itself: the adapter is released, the Mac returns to normal charging under
your charge limit. No confirmation, nothing left to turn off. - "The moment" means the next check — the battery can drift ~1–2% past the
floor between checks. - Below 20% macOS clips the reported charge and the gauge gets less
accurate; 1% is the absolute lowest value BatteryControl will ever
program, and macOS shuts down on its own before 0%.
Nothing else changed
- Same daemon control logic, verification, and compatibility behavior as 1.1.0.
- 267 tests passing, zero compiler warnings, Release build clean.
- Physically verified configuration remains the author's M3 MacBook Air
(Mac15,13, mBoot-20457.1.29, macOS 15.8); everything else is
capability-tested. Compatibility reports grow the verified list —
batterycontrol compatibility --report.
1.1.0 — compatibility workflow, database install, calibration CLI, update check
BatteryControl 1.1.0
Surface-parity and evidence-workflow release on the 1.0.2 base. No new
privileged mechanisms — the daemon still owns every hardware operation, and
unknown firmware stays read-only.
Compatibility reports, end to end
The report that feeds the compatibility database used to require running the
daemon binary by hand. Now there are three ways to get the identical JSON,
one shared generator:
batterycontrol compatibility --report- Diagnostics → Export Compatibility Report… in the app
- the original
sudo com.batterycontrol.daemon --export-compat-report <path>
It contains your chip, model identifier, macOS and firmware builds, and the
SMC control-key signature. No serial numbers, no UUIDs, no usernames — a
privacy gate refuses to emit PII-shaped keys. A new GitHub issue template
mirrors the JSON fields, so a report lands ready to review.
Database install without an app update
When a new Mac gets verified, you shouldn't have to wait for the next app
release to be recognized. Install a reviewed database directly:
batterycontrol database install compatibility.json
or Settings → Compatibility Database → Install Database File…. The
privileged daemon validates the whole payload (schema, per-profile structure,
evidence requirements), writes the root-owned file atomically, activates it
in-process, and re-classifies the machine. Bad payloads are rejected with the
specific reason — schema mismatch, empty key sets, evidence missing.
The boundary is unchanged: a database entry broadens recognition only. Your
machine's hardware is still probed at runtime and every write is still
verified by readback before anything reports as active.
Calibration from the terminal
batterycontrol calibration status | start | cancel — the same guided cycle
the GUI runs (discharge to 20% → charge to 100% → hold 3 hours → drop to your
limit). The daemon owns the sequence and its safety gates; the CLI adds no
validation of its own.
Update check (passive)
A daily query to GitHub Releases, shown in Settings with a link when a newer
version exists (batterycontrol update-check works from the terminal too).
Default on, with a setting to turn it off entirely — disabling stops all
update-related requests. Nothing is ever downloaded or installed
automatically; in-place upgrades are exactly what the 1.0.2 stale-process
work made survivable, and updating remains a deliberate user action.
Native Charge Limit on macOS 26.4+ — documented, not claimed
The ownership model (BatteryControl vs Apple's built-in limit) stays
code-tested only. The physical test plan for a real 26.4+ machine now ships
at Docs/NativeChargeLimitTestPlan.md. Until someone runs it, the daemon
reports Apple's limit state as "could not be read" rather than guessing.
Validation
- 267 tests passing (23 new), zero compiler warnings, Release build clean
- Verified M3 profile (Mac15,13, mBoot-20457.1.29, macOS 15.8) re-validated
on the shipping binaries: report emitted with the live signature, bad
database rejected, shipped database installed, 80/70 limit Active and
hardware-verified throughout - Physical validation remains exactly one configuration; everything else is
capability-tested. Compatibility reports are how that changes.
Fixed
- Version mismatch after a pkg upgrade now resolves through the app's
existing Repair Helper flow (the pkg replaces the app; Repair Helper
replaces the installed daemon) — validated end to end this release.
SHA-256: see SHA256SUMS in the assets. GUI pkg contains app + daemon +
CLI; the CLI-only pkg contains no daemon.
1.0.2
BatteryControl 1.0.2
Compatibility, reliability, and hardware-validation release after the platform
expansion in 1.0.1. No new features on purpose.
Reliability fixes found on real hardware
- No more false "verification failed" alarms. When the firmware pauses an
active charge, the charge current tapers over up to ~2 minutes before macOS
reports the change. The old 60-second observation window gave up early,
reported the limit as unverified, and then silently recovered a few minutes
later. The observation window is now long enough for the measured hardware
behavior (still bounded; a real failure is still reported as a failure, and
a failed write still deactivates the limit). Verified on hardware: the
transition that used to false-alarm now confirms without one. - CLI force-charge. The GUI could charge past the limit; the CLI could not.
batterycontrol charge start [pct]andcharge stopnow exist, same path
and same safety gates as the GUI. - App survives in-place upgrades. If BatteryControl was updated while it
was running, the daemon (correctly) refused the old process's connections
forever, and the app silently retried. The daemon now logs why a client was
refused, and the app shows a "was updated while running" banner with a
one-click Reopen button. The accept/reject security decision is unchanged.
Physically validated this release
On the author's MacBook Air (Mac15,13, Apple M3, macOS 15.8, mBoot-20457.1.29
— the one verified profile):
- Charge limits 60/50, 70/60, 80/70, 90/98, and 100/98 programmed, read back,
and confirmed against actual battery behavior — including limits below
Apple's native 80% floor. - Force-charge climbed 81% → 84% past an active 80% limit, then
charge stop
re-engaged the limit at 84%. - The limit held through sleep and wake on AC and on battery; the daemon kept
its state, and no write storm followed the wake. - Daemon restart recovery confirmed on the shipping binaries (policy restored
from the persisted store, limit active and verified).
Compatibility status (unchanged in scope, more evidence in the bank)
- Platform scope: Apple Silicon M1–M5, macOS 14/15/26/27. Scope is wider
than the validated hardware; runtime capability probing decides what a
given machine can do, and unknown firmware stays read-only by design. - Verified: the M3 / macOS 15.8 / mBoot-20457.1.29 configuration above.
- Compatible by capability: machines whose SMC keys probe cleanly. Not
physically validated by the project. - 244 unit tests — up from 225 — including new coverage for the
verification window, the CLI charge commands, the rejection classifier, and
corrupt/interrupted policy-store recovery. Tests are not the same thing as
physical validation, and the compatibility tiers keep that distinction.
Known limitations
- Only one hardware configuration is physically verified. Reports from other
M1–M5 Macs (via the compatibility report workflow in CONTRIBUTING) are how
the verified list grows. - Binaries are signed with an Apple Development identity, not notarized.
Gatekeeper asks for approval on first install; SHA256SUMS is attached. batterycontrol --test-inhibitintentionally probes the legacy key family
and will honestly report failure on modern firmware — that is what it is
for.
Full changelog: compare with 1.0.1 — c885196...c99b8f8.
BatteryControl 1.0.1 — macOS 14/15/26/27 and M5 support
BatteryControl 1.0.1
This release widens the range of systems BatteryControl will run on — and keeps the honesty rules that got 1.0.0 out the door.
What changed
- Supported OS versions are now macOS 14 Sonoma, 15 Sequoia, 26 Tahoe, and 27. The platform gate admits all four; what BatteryControl can actually do is still decided at runtime by probing which SMC mechanisms your firmware exposes. An unknown key signature gets read-only diagnostics on any of these OS versions.
- M5-generation Macs are now admitted too. The old exclusion existed because M5 Macs couldn't run macOS 15 — with macOS 26/27 in scope that reason is gone. Same rule as everything else: chip generation gets you in the door, capability decides what happens next. No M5 Mac is in the verified evidence set, so none can be reported as verified.
- Apple's built-in Charge Limit (macOS 26.4+, 80–100%) is now accounted for. BatteryControl never writes that setting. When you have no BatteryControl limit set, the app and CLI tell you who is actually managing charging; when you set a BatteryControl limit, the daemon verifies it against the real battery state and is the authoritative controller.
- Custom limits below Apple's 80% floor are the headline use: presets are now 60/70/75/80/85/90/95/100 (plus custom), with the same hysteresis, readback verification, and safety floor as before.
- Minimum macOS is now 14. The whole app builds for it with zero availability shims — no 15-only APIs were in use.
Honest status
- Physically verified: one configuration. M3 MacBook Air (Mac15,13), mBoot-20457.1.29, macOS 15.8 (24H23). Firmware-managed limit + CHIE discharge, with full write/readback evidence.
- Capability-tested, never physically run: every macOS 14/26/27 combination, and every M5 Mac. The classification logic, ownership model, and per-write verification are unit-tested for them (225 tests), but that is not a substitute for hardware. If your Mac reports "compatible by capability", control is enabled with per-write verification; if it reports untested, it stays read-only.
- Compatibility reports from other machines are what grow the verified set — the read-only export command is in the README.
Fixed in 1.0.1 (already shipped in 1.0.0 artifacts, formalized here)
None — 1.0.0 had no known defects. This release is scope expansion plus version alignment.
Checksums
Verify with shasum -a 256 -c SHA256SUMS — attached, covering all three artifacts. Packages remain unsigned (no Developer ID credentials); Gatekeeper will ask for approval on first install.