macOS IOKit Power Sources: Battery Snapshots, Estimates, and Change Notifications
Build macOS power-source snapshots with IOKit, handle missing keys and estimate sentinels, and add change notifications without confusing charge with health.
macOS exposes a read-oriented power-source API through IOKit. It can return a snapshot of connected power sources, per-source dictionaries, an operating-system estimate of remaining time, and notifications when power-source information changes. This is a useful foundation for a menu-bar status display, a diagnostic utility, or a workload policy that responds to an unplug event.
The API is not a battery-health oracle and its fields are not a stable promise that every source reports every value. Apple documents batteries and UPS devices as power sources, and explicitly warns that individual dictionaries may omit keys. Treat each read as telemetry with provenance and uncertainty, not as a hardware truth suitable for a warranty judgment or a critical shutdown deadline.
Snapshot ownership and dictionary lifetime
The normal sequence starts with IOPSCopyPowerSourcesInfo(), an opaque snapshot token. Pass it to IOPSCopyPowerSourcesList() to obtain handles, then pass each handle with the snapshot to IOPSGetPowerSourceDescription(). Do not inspect the opaque token directly and do not treat a handle as a dictionary key. Apple’s accessors define the supported path.
Core Foundation ownership rules matter here. Release the values returned by the two Copy functions when finished. The per-source dictionary returned by IOPSGetPowerSourceDescription() is owned by the snapshot and must not be released separately. Handles obtained from the copied array are borrowed while that array remains alive. If a function fails and returns NULL, stop that stage cleanly instead of dereferencing it.
The following C example prints raw current and maximum capacity values only when both are valid numbers. It deliberately avoids hard-coding units: Apple-defined sources publish capacity as a percentage, while a source implementation may define another unit, provided its capacity fields use consistent units.
#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/ps/IOPowerSources.h>
#include <IOKit/ps/IOPSKeys.h>
#include <stdio.h>
static int read_integer(CFDictionaryRef dictionary, CFStringRef key, int *out)
{
CFTypeRef value = CFDictionaryGetValue(dictionary, key);
if (value == NULL || CFGetTypeID(value) != CFNumberGetTypeID())
return 0;
return CFNumberGetValue((CFNumberRef)value, kCFNumberIntType, out);
}
int main(void)
{
CFTypeRef snapshot = IOPSCopyPowerSourcesInfo();
if (snapshot == NULL) {
fputs("could not read power-source snapshot\n", stderr);
return 1;
}
CFArrayRef sources = IOPSCopyPowerSourcesList(snapshot);
if (sources == NULL) {
CFRelease(snapshot);
fputs("could not list power sources\n", stderr);
return 1;
}
for (CFIndex i = 0; i < CFArrayGetCount(sources); ++i) {
CFTypeRef handle = CFArrayGetValueAtIndex(sources, i); /* borrowed */
CFDictionaryRef description =
IOPSGetPowerSourceDescription(snapshot, handle); /* borrowed */
if (description == NULL)
continue;
int current = 0;
int maximum = 0;
if (read_integer(description, CFSTR(kIOPSCurrentCapacityKey), ¤t) &&
read_integer(description, CFSTR(kIOPSMaxCapacityKey), &maximum)) {
printf("source[%ld]: current=%d max=%d\n", (long)i, current, maximum);
} else {
printf("source[%ld]: capacity unavailable\n", (long)i);
}
}
CFRelease(sources);
CFRelease(snapshot);
return 0;
}
Compile this as a small diagnostic with Apple’s SDK and the IOKit and Core Foundation frameworks, then compare its output with the built-in view:
clang -Wall -Wextra power_snapshot.c -framework IOKit \
-framework CoreFoundation -o power_snapshot
./power_snapshot
pmset -g batt
If a product displays a percentage, calculate it only when the source’s current and maximum values are numeric and the source contract makes their units comparable. A zero or missing maximum is not a division case. Preserve the raw values and the source type in logs so the derived percentage can be audited later. Do not identify a UPS or external accessory as an internal Mac battery solely because it contains capacity fields.
Estimates are estimates, not deadlines
IOPSGetTimeRemainingEstimate() provides a quick estimate for the currently attached power sources. It can return kIOPSTimeRemainingUnknown while macOS is still calculating after an event such as wake or a change in external power. It can return kIOPSTimeRemainingUnlimited when an unlimited source is available. The regular numeric result is an operating-system estimate, not a guarantee of how long a particular workload will run.
There is a documentation wording nuance worth preserving: the function’s summary describes remaining minutes, while its return-value section describes the positive CFTimeInterval as seconds. Code should follow the SDK contract for the target OS and represent the unknown and unlimited sentinels separately before formatting a value. Do not display a sentinel as a huge number of minutes, and do not assume the estimate is updated on an exact periodic schedule.
For a user-facing decision, pair the estimate with charging state and current-source state when those keys are present. Keep a separate “estimate unavailable” state rather than replacing it with zero. A background worker that must save work should use its own measured workload and a conservative policy; it should not promise a fixed shutdown time based only on this estimate.
Respond to changes without polling constantly
IOPSNotificationCreateRunLoopSource() returns a Core Foundation run-loop source whose callback fires when a power source is added, removed, or changed. The caller owns and must release the returned source. Add it to an appropriate run loop, and in the callback request a fresh snapshot rather than assuming the callback carries a complete set of new values. If the application does not use a run loop, Apple documents other notification mechanisms, including the notify interface and kIOPSTimeRemainingNotificationKey for time-remaining changes.
Apple notes that this run-loop notification can fire fairly frequently while a battery is charging or discharging because percentage or time estimates change. The same header is internally inconsistent about IOPSCreateLimitedPowerNotification(): the preceding recommendation describes limited-to-unlimited changes, while that function’s abstract and callback describe unlimited AC-to-limited battery or UPS (AC-to-DC). Do not assume this narrower source reports both directions; verify the behavior required on the target SDK and OS, or retain the general change notification when both directions matter.
Callbacks should be treated as invalidation signals. Coalesce bursts, refresh the snapshot once on the owning queue, and compare the result with the last snapshot. A source can disappear between notification delivery and a later query, so every copy, array lookup, and dictionary lookup still needs a failure path. Tear down the source before releasing callback context so a late callback cannot access freed state.
A practical test matrix changes the power state on a test Mac: external power connected, disconnected, reconnected, sleep and wake, a source with no estimate key, and a system where no internal battery is exposed. Verify that the UI handles missing dictionaries and repeated notifications, and that the callback is removed during shutdown. Do not use a battery-drain test as the only test of error handling.
Capacity is not battery health
kIOPSCurrentCapacityKey describes current capacity. kIOPSMaxCapacityKey describes maximum or full-charge capacity. Apple documents that Apple-defined sources publish these capacity values in percent, and that a source implementation can define units subject to consistency. These fields can support a current-charge display, but they do not alone diagnose cell aging, predict serviceability, or establish a battery-health condition.
The header also defines separate health-related keys, including a health estimate and confidence fields, but the API’s general warning still applies: do not expect every source to provide every key. A missing health value means unavailable, not “good” or “bad.” For a repair decision, use the supported macOS battery condition view and the appropriate service procedure; do not invent a threshold from the current-capacity snapshot.
Power-source dictionaries may include identifiers or vendor-provided details. Store only fields needed for the stated feature, avoid logging serial-like values by default, and define retention before exporting diagnostics. A low-overhead display generally needs only source state, charging state, current/max capacity, and the estimate sentinel/value.
The resulting design is small but disciplined: take a copied snapshot, use Apple’s accessors, tolerate optional fields, preserve ownership boundaries, refresh on notifications, and keep live charge telemetry separate from health diagnostics.
Related:
- macOS Power Assertions: Preventing Sleep Without Hiding the Reason
- ProcessInfo Thermal State on macOS: Adaptive Workloads Without Fan Guesswork
Sources:
- Apple
IOPowerSources.h- API scope, source types, and Core Foundation Copy/Get ownership rules. - Apple
IOPSCopyPowerSourcesInfo- opaque snapshot contract and required release behavior. - Apple
IOPSCopyPowerSourcesList- returned handles, array ownership, and accessor usage. - Apple
IOPSGetPowerSourceDescription- borrowed dictionary lifetime and warning that keys can be absent. - Apple
IOPSKeys.h- capacity, state, charging, health, and time-remaining keys. - Apple
kIOPSCurrentCapacityKey- capacity units and the relationship to maximum capacity. - Apple
IOPSGetTimeRemainingEstimate- unknown and unlimited sentinels and positive-return time unit documentation. - Apple
IOPSNotificationCreateRunLoopSource- change callback, run-loop integration, alternatives, and release requirement. - Apple open-source
pmsetimplementation - source for thebattgetter used by thepmset -g battcomparison command. - Apple: Check the condition of your Mac laptop’s battery - user-facing battery condition context; not a substitute for the programmatic API contract.