180 lines
6.6 KiB
Objective-C
180 lines
6.6 KiB
Objective-C
//
|
|
// MMBatteryTelemetryProvider.m
|
|
// MacMonitor
|
|
//
|
|
// Telemetry provider for AppleSmartBattery health, cycle count, capacity, and adapter info.
|
|
//
|
|
|
|
#import "MMBatteryTelemetryProvider.h"
|
|
#import <IOKit/IOKitLib.h>
|
|
#import <IOKit/ps/IOPowerSources.h>
|
|
#import <IOKit/ps/IOPSKeys.h>
|
|
|
|
@implementation MMBatteryTelemetryProvider
|
|
|
|
- (instancetype)init {
|
|
self = [super init];
|
|
return self;
|
|
}
|
|
|
|
- (MMTelemetryDomain)domain {
|
|
return MMTelemetryDomainPower;
|
|
}
|
|
|
|
- (NSString *)providerIdentifier {
|
|
return @"com.i3omb.macmonitor.telemetry.battery";
|
|
}
|
|
|
|
- (BOOL)isAvailable {
|
|
io_service_t service = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("AppleSmartBattery"));
|
|
if (service != IO_OBJECT_NULL) {
|
|
IOObjectRelease(service);
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (BOOL)hasBattery {
|
|
return [self isAvailable];
|
|
}
|
|
|
|
+ (NSDictionary<NSString *, id> *)calculateBatteryHealthWithProperties:(nullable NSDictionary<NSString *, id> *)rawProps {
|
|
if (!rawProps || rawProps.count == 0) {
|
|
return @{
|
|
@"hasBattery": @(NO),
|
|
@"installed": @(NO),
|
|
@"healthCondition": @"No Battery",
|
|
@"healthPercent": @(0.0),
|
|
@"cycleCount": @(0),
|
|
@"designCycleCount": @(0),
|
|
@"currentCapacity": @(0),
|
|
@"maxCapacity": @(0),
|
|
@"designCapacity": @(0),
|
|
@"temperature": @(0.0),
|
|
@"voltage": @(0.0),
|
|
@"amperage": @(0.0),
|
|
@"watts": @(0.0),
|
|
@"isCharging": @(NO),
|
|
@"isCharged": @(NO),
|
|
@"externalConnected": @(NO),
|
|
@"timeRemainingMinutes": @(-1),
|
|
@"manufacturer": @"N/A",
|
|
@"serial": @"N/A",
|
|
@"deviceName": @"N/A"
|
|
};
|
|
}
|
|
|
|
BOOL installed = [rawProps[@"BatteryInstalled"] boolValue];
|
|
NSInteger cycleCount = [rawProps[@"CycleCount"] integerValue];
|
|
NSInteger designCycleCount = rawProps[@"DesignCycleCount9C"] ? [rawProps[@"DesignCycleCount9C"] integerValue] : 1000;
|
|
|
|
NSInteger currentCapacity = [rawProps[@"CurrentCapacity"] integerValue];
|
|
NSInteger maxCapacity = [rawProps[@"MaxCapacity"] integerValue];
|
|
|
|
// DesignCapacity might be at top level or inside BatteryData
|
|
NSInteger designCapacity = [rawProps[@"DesignCapacity"] integerValue];
|
|
if (designCapacity <= 0 && [rawProps[@"BatteryData"] isKindOfClass:[NSDictionary class]]) {
|
|
designCapacity = [rawProps[@"BatteryData"][@"DesignCapacity"] integerValue];
|
|
}
|
|
|
|
// Health percentage = (MaxCapacity / DesignCapacity) * 100.0
|
|
double healthPercent = 0.0;
|
|
if (designCapacity > 0) {
|
|
healthPercent = ((double)maxCapacity / (double)designCapacity) * 100.0;
|
|
if (healthPercent > 100.0) healthPercent = 100.0;
|
|
} else if (maxCapacity > 0) {
|
|
healthPercent = 100.0;
|
|
}
|
|
|
|
// Health condition evaluation
|
|
NSString *condition = @"Normal";
|
|
BOOL permanentFailure = [rawProps[@"PermanentFailureStatus"] integerValue] != 0;
|
|
if (permanentFailure) {
|
|
condition = @"Permanent Failure";
|
|
} else if (healthPercent < 70.0 || (designCycleCount > 0 && cycleCount > designCycleCount)) {
|
|
condition = @"Service Recommended";
|
|
} else if (healthPercent < 80.0) {
|
|
condition = @"Fair";
|
|
} else {
|
|
condition = @"Normal";
|
|
}
|
|
|
|
// Temperature: AppleSmartBattery returns in units of 0.01 deg C or 0.1 deg C (e.g. 3151 = 31.51 deg C)
|
|
double rawTemp = [rawProps[@"Temperature"] doubleValue];
|
|
double tempC = rawTemp > 1000.0 ? (rawTemp / 100.0) : (rawTemp > 100.0 ? rawTemp / 10.0 : rawTemp);
|
|
|
|
double voltage = [rawProps[@"Voltage"] doubleValue] / 1000.0; // mV -> V
|
|
|
|
// Amperage can be signed 64-bit stored as unsigned in dict
|
|
int64_t rawAmp = [rawProps[@"InstantAmperage"] longLongValue];
|
|
if (rawAmp == 0) {
|
|
rawAmp = [rawProps[@"Amperage"] longLongValue];
|
|
}
|
|
int32_t signedAmp = (int32_t)rawAmp; // standard cast from uint32/uint64 representation
|
|
double amperage = ((double)signedAmp) / 1000.0; // mA -> A
|
|
double watts = fabs(voltage * amperage);
|
|
|
|
BOOL isCharging = [rawProps[@"IsCharging"] boolValue];
|
|
BOOL fullyCharged = [rawProps[@"FullyCharged"] boolValue];
|
|
BOOL externalConnected = [rawProps[@"ExternalConnected"] boolValue];
|
|
NSInteger timeRemaining = [rawProps[@"TimeRemaining"] integerValue];
|
|
if (timeRemaining == 65535) {
|
|
timeRemaining = -1;
|
|
}
|
|
|
|
NSString *manufacturer = rawProps[@"Manufacturer"] ?: @"Apple";
|
|
NSString *serial = rawProps[@"Serial"] ?: @"";
|
|
NSString *deviceName = rawProps[@"DeviceName"] ?: @"";
|
|
|
|
// Power adapter details
|
|
NSDictionary *adapterDetails = [rawProps[@"AdapterDetails"] isKindOfClass:[NSDictionary class]] ? rawProps[@"AdapterDetails"] : nil;
|
|
NSInteger adapterWatts = 0;
|
|
if (adapterDetails && adapterDetails[@"Watts"]) {
|
|
adapterWatts = [adapterDetails[@"Watts"] integerValue];
|
|
}
|
|
|
|
return @{
|
|
@"hasBattery": @(YES),
|
|
@"installed": @(installed),
|
|
@"healthCondition": condition,
|
|
@"healthPercent": @(round(healthPercent * 10.0) / 10.0),
|
|
@"cycleCount": @(cycleCount),
|
|
@"designCycleCount": @(designCycleCount),
|
|
@"currentCapacity": @(currentCapacity),
|
|
@"maxCapacity": @(maxCapacity),
|
|
@"designCapacity": @(designCapacity),
|
|
@"temperature": @(round(tempC * 10.0) / 10.0),
|
|
@"voltage": @(round(voltage * 100.0) / 100.0),
|
|
@"amperage": @(round(amperage * 100.0) / 100.0),
|
|
@"watts": @(round(watts * 10.0) / 10.0),
|
|
@"isCharging": @(isCharging),
|
|
@"isCharged": @(fullyCharged),
|
|
@"externalConnected": @(externalConnected),
|
|
@"timeRemainingMinutes": @(timeRemaining),
|
|
@"manufacturer": manufacturer,
|
|
@"serial": serial,
|
|
@"deviceName": deviceName,
|
|
@"adapterWatts": @(adapterWatts)
|
|
};
|
|
}
|
|
|
|
- (nullable NSDictionary<NSString *, id> *)sampleTelemetryWithError:(NSError **)error {
|
|
io_service_t service = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("AppleSmartBattery"));
|
|
if (service == IO_OBJECT_NULL) {
|
|
return [MMBatteryTelemetryProvider calculateBatteryHealthWithProperties:nil];
|
|
}
|
|
|
|
CFMutableDictionaryRef props = NULL;
|
|
kern_return_t kr = IORegistryEntryCreateCFProperties(service, &props, kCFAllocatorDefault, 0);
|
|
IOObjectRelease(service);
|
|
|
|
if (kr != KERN_SUCCESS || !props) {
|
|
return [MMBatteryTelemetryProvider calculateBatteryHealthWithProperties:nil];
|
|
}
|
|
|
|
NSDictionary *dict = (__bridge_transfer NSDictionary *)props;
|
|
return [MMBatteryTelemetryProvider calculateBatteryHealthWithProperties:dict];
|
|
}
|
|
|
|
@end
|