MacBook Battery Health, Cycle Count & Power Adapter Telemetry #17

Closed
opened 2026-09-08 10:29:18 +01:00 by gronod · 1 comment
Owner

Resolved in Milestone 3. Implemented MMBatteryTelemetryProvider.h/.m querying IOKit AppleSmartBattery properties, cycle counts, maximum and design capacity, health condition evaluation, voltage, wattage, and power adapter status. Verified with unit tests. Merged into develop.

Resolved in Milestone 3. Implemented `MMBatteryTelemetryProvider.h/.m` querying IOKit `AppleSmartBattery` properties, cycle counts, maximum and design capacity, health condition evaluation, voltage, wattage, and power adapter status. Verified with unit tests. Merged into `develop`.
Author
Owner

Technical Implementation Plan & Code Solution

1. AppleSmartBattery & Power Source Architecture

On Intel MacBooks, power metrics are delivered through:

  • IOPowerSources.h for high-level battery percentages and charger attachment.
  • IORegistry AppleSmartBattery service for hardware telemetry (cycle count, cell voltage, wear level, wattage).
  • Desktop Macs (iMac, Mac Pro, Mac mini) have no AppleSmartBattery node and are handled gracefully.

2. Objective-C Provider (MMBatteryProvider.m)

#import "MMBatteryProvider.h"
#import <IOKit/IOKitLib.h>
#import <IOKit/ps/IOPowerSources.h>
#import <IOKit/ps/IOPSKeys.h>

@implementation MMBatteryProvider

- (nullable NSDictionary<NSString *, id> *)sampleTelemetryWithError:(NSError **)error {
    io_service_t service = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("AppleSmartBattery"));
    if (!service) {
        return nil; // Desktop Mac without internal battery
    }

    CFMutableDictionaryRef props = NULL;
    kern_return_t kr = IORegistryEntryCreateCFProperties(service, &props, kCFAllocatorDefault, 0);
    IOObjectRelease(service);

    if (kr != KERN_SUCCESS || !props) return nil;

    NSDictionary *dict = (__bridge_transfer NSDictionary *)props;

    int currentCap = [dict[@"CurrentCapacity"] intValue];
    int maxCap     = [dict[@"MaxCapacity"] intValue];
    int designCap  = [dict[@"DesignCapacity"] intValue];
    int cycleCount = [dict[@"CycleCount"] intValue];
    int voltageMv  = [dict[@"Voltage"] intValue];
    int amperageMa = [dict[@"InstantAmperage"] intValue];
    int rawTemp    = [dict[@"Temperature"] intValue]; // Tenths of degree K or C
    BOOL isCharging = [dict[@"IsCharging"] boolValue];
    BOOL externalConnected = [dict[@"ExternalConnected"] boolValue];

    NSDictionary *adapter = dict[@"AdapterDetails"];
    int adapterWatts = [adapter[@"Watts"] intValue];

    double healthPercent = (designCap > 0) ? ((double)maxCap / (double)designCap) * 100.0 : 100.0;
    double chargePercent = (maxCap > 0) ? ((double)currentCap / (double)maxCap) * 100.0 : 0.0;
    double powerWatts = (fabs((double)amperageMa) * (double)voltageMv) / 1_000_000.0;
    double tempC = (rawTemp > 1000) ? ((rawTemp - 2731.5) / 10.0) : ((double)rawTemp / 100.0);

    return @{
        @"hasBattery": @(YES),
        @"chargePercent": @(chargePercent),
        @"healthPercent": @(fmin(100.0, healthPercent)),
        @"cycleCount": @(cycleCount),
        @"isCharging": @(isCharging),
        @"isACConnected": @(externalConnected),
        @"powerWatts": @(powerWatts),
        @"voltageVolts": @((double)voltageMv / 1000.0),
        @"temperatureC": @(tempC),
        @"adapterWatts": @(adapterWatts)
    };
}
@end

3. Swift UI Integration

public struct BatterySnapshot: Sendable {
    public let chargePercent: Double
    public let healthPercent: Double
    public let cycleCount: Int
    public let isCharging: Bool
    public let isACConnected: Bool
    public let powerWatts: Double
    public let adapterWatts: Int
}

4. Desktop Mac Handling

If AppleSmartBattery returns 0 services, the provider immediately yields nil, allowing SwiftUI views to hide battery cards cleanly when running on an iMac, Mac mini, or Mac Pro.

## Technical Implementation Plan & Code Solution ### 1. AppleSmartBattery & Power Source Architecture On Intel MacBooks, power metrics are delivered through: - `IOPowerSources.h` for high-level battery percentages and charger attachment. - IORegistry `AppleSmartBattery` service for hardware telemetry (cycle count, cell voltage, wear level, wattage). - Desktop Macs (iMac, Mac Pro, Mac mini) have no `AppleSmartBattery` node and are handled gracefully. --- ### 2. Objective-C Provider (`MMBatteryProvider.m`) ```objc #import "MMBatteryProvider.h" #import <IOKit/IOKitLib.h> #import <IOKit/ps/IOPowerSources.h> #import <IOKit/ps/IOPSKeys.h> @implementation MMBatteryProvider - (nullable NSDictionary<NSString *, id> *)sampleTelemetryWithError:(NSError **)error { io_service_t service = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("AppleSmartBattery")); if (!service) { return nil; // Desktop Mac without internal battery } CFMutableDictionaryRef props = NULL; kern_return_t kr = IORegistryEntryCreateCFProperties(service, &props, kCFAllocatorDefault, 0); IOObjectRelease(service); if (kr != KERN_SUCCESS || !props) return nil; NSDictionary *dict = (__bridge_transfer NSDictionary *)props; int currentCap = [dict[@"CurrentCapacity"] intValue]; int maxCap = [dict[@"MaxCapacity"] intValue]; int designCap = [dict[@"DesignCapacity"] intValue]; int cycleCount = [dict[@"CycleCount"] intValue]; int voltageMv = [dict[@"Voltage"] intValue]; int amperageMa = [dict[@"InstantAmperage"] intValue]; int rawTemp = [dict[@"Temperature"] intValue]; // Tenths of degree K or C BOOL isCharging = [dict[@"IsCharging"] boolValue]; BOOL externalConnected = [dict[@"ExternalConnected"] boolValue]; NSDictionary *adapter = dict[@"AdapterDetails"]; int adapterWatts = [adapter[@"Watts"] intValue]; double healthPercent = (designCap > 0) ? ((double)maxCap / (double)designCap) * 100.0 : 100.0; double chargePercent = (maxCap > 0) ? ((double)currentCap / (double)maxCap) * 100.0 : 0.0; double powerWatts = (fabs((double)amperageMa) * (double)voltageMv) / 1_000_000.0; double tempC = (rawTemp > 1000) ? ((rawTemp - 2731.5) / 10.0) : ((double)rawTemp / 100.0); return @{ @"hasBattery": @(YES), @"chargePercent": @(chargePercent), @"healthPercent": @(fmin(100.0, healthPercent)), @"cycleCount": @(cycleCount), @"isCharging": @(isCharging), @"isACConnected": @(externalConnected), @"powerWatts": @(powerWatts), @"voltageVolts": @((double)voltageMv / 1000.0), @"temperatureC": @(tempC), @"adapterWatts": @(adapterWatts) }; } @end ``` --- ### 3. Swift UI Integration ```swift public struct BatterySnapshot: Sendable { public let chargePercent: Double public let healthPercent: Double public let cycleCount: Int public let isCharging: Bool public let isACConnected: Bool public let powerWatts: Double public let adapterWatts: Int } ``` --- ### 4. Desktop Mac Handling If `AppleSmartBattery` returns 0 services, the provider immediately yields `nil`, allowing SwiftUI views to hide battery cards cleanly when running on an iMac, Mac mini, or Mac Pro.
gronod added this to the M3: Advanced Kernel, Process & Peripheral Telemetry milestone 2026-09-08 10:42:37 +01:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: gronod/MacMonitor#17