315 lines
10 KiB
Objective-C
315 lines
10 KiB
Objective-C
#import "MMPowerTelemetryProvider.h"
|
|
#import <IOKit/IOKitLib.h>
|
|
#import <IOKit/ps/IOPowerSources.h>
|
|
#import <IOKit/ps/IOPSKeys.h>
|
|
|
|
@interface MMPowerTelemetryProvider () {
|
|
MMAppleSMCClient *_smcClient;
|
|
NSMutableArray<NSString *> *_discoveredPowerKeys;
|
|
NSMutableArray<NSString *> *_discoveredVoltageKeys;
|
|
NSMutableArray<NSString *> *_discoveredCurrentKeys;
|
|
BOOL _probed;
|
|
}
|
|
@end
|
|
|
|
@implementation MMPowerTelemetryProvider
|
|
|
|
- (instancetype)init {
|
|
return [self initWithSMCClient:[MMAppleSMCClient sharedClient]];
|
|
}
|
|
|
|
- (instancetype)initWithSMCClient:(MMAppleSMCClient *)client {
|
|
self = [super init];
|
|
if (self) {
|
|
_smcClient = client;
|
|
_discoveredPowerKeys = [NSMutableArray array];
|
|
_discoveredVoltageKeys = [NSMutableArray array];
|
|
_discoveredCurrentKeys = [NSMutableArray array];
|
|
[self probeSensors];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)probeSensors {
|
|
if (!_smcClient.isAvailable) return;
|
|
|
|
NSArray<NSString *> *powerCandidates = @[
|
|
@"PSTR", @"PDTR", @"PCPR", @"PCTR", @"PC0C", @"PCPT",
|
|
@"PG0R", @"PGTR", @"PMSR", @"PM0R", @"PCPC"
|
|
];
|
|
|
|
NSArray<NSString *> *voltageCandidates = @[
|
|
@"VC0C", @"VD0R", @"VN0C", @"VM0R", @"VP0R"
|
|
];
|
|
|
|
NSArray<NSString *> *currentCandidates = @[
|
|
@"IC0C", @"ID0R", @"IG0C", @"IM0R"
|
|
];
|
|
|
|
[_discoveredPowerKeys removeAllObjects];
|
|
for (NSString *key in powerCandidates) {
|
|
NSNumber *val = [_smcClient readNumericValueForKey:key error:nil];
|
|
if (val && [val doubleValue] > 0.0 && [val doubleValue] < 5000.0) {
|
|
[_discoveredPowerKeys addObject:key];
|
|
}
|
|
}
|
|
|
|
[_discoveredVoltageKeys removeAllObjects];
|
|
for (NSString *key in voltageCandidates) {
|
|
NSNumber *val = [_smcClient readNumericValueForKey:key error:nil];
|
|
if (val && [val doubleValue] > 0.0) {
|
|
[_discoveredVoltageKeys addObject:key];
|
|
}
|
|
}
|
|
|
|
[_discoveredCurrentKeys removeAllObjects];
|
|
for (NSString *key in currentCandidates) {
|
|
NSNumber *val = [_smcClient readNumericValueForKey:key error:nil];
|
|
if (val && [val doubleValue] > 0.0) {
|
|
[_discoveredCurrentKeys addObject:key];
|
|
}
|
|
}
|
|
|
|
_probed = YES;
|
|
}
|
|
|
|
- (MMTelemetryDomain)domain {
|
|
return MMTelemetryDomainPower;
|
|
}
|
|
|
|
- (NSString *)providerIdentifier {
|
|
return @"com.i3omb.macmonitor.telemetry.power";
|
|
}
|
|
|
|
- (BOOL)isAvailable {
|
|
return _smcClient.isAvailable;
|
|
}
|
|
|
|
- (MMAppleSMCClient *)smcClient {
|
|
return _smcClient;
|
|
}
|
|
|
|
- (NSArray<NSString *> *)discoveredPowerKeys {
|
|
return [_discoveredPowerKeys copy];
|
|
}
|
|
|
|
- (NSArray<NSString *> *)discoveredVoltageKeys {
|
|
return [_discoveredVoltageKeys copy];
|
|
}
|
|
|
|
- (NSArray<NSString *> *)discoveredCurrentKeys {
|
|
return [_discoveredCurrentKeys copy];
|
|
}
|
|
|
|
+ (NSString *)unitForKey:(NSString *)key {
|
|
if ([key hasPrefix:@"P"]) return @"W";
|
|
if ([key hasPrefix:@"V"]) return @"V";
|
|
if ([key hasPrefix:@"I"]) return @"A";
|
|
return @"";
|
|
}
|
|
|
|
+ (NSString *)humanReadableNameForKey:(NSString *)key {
|
|
static NSDictionary<NSString *, NSString *> *names = nil;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
names = @{
|
|
@"PSTR": @"Total System Power",
|
|
@"PDTR": @"DC-In Total Power",
|
|
@"PCPR": @"CPU Package Power",
|
|
@"PCTR": @"CPU Total Power",
|
|
@"PC0C": @"CPU Core Power",
|
|
@"PCPT": @"CPU Package Total Power",
|
|
@"PG0R": @"GPU 0 Power",
|
|
@"PGTR": @"GPU Total Power",
|
|
@"PMSR": @"Memory Subsystem Power",
|
|
@"PM0R": @"Memory Rail Power",
|
|
@"PCPC": @"Power Supply In / AC",
|
|
@"VC0C": @"CPU Core Voltage",
|
|
@"VD0R": @"DC-In Rail Voltage",
|
|
@"VN0C": @"PCH Rail Voltage",
|
|
@"VM0R": @"Memory Rail Voltage",
|
|
@"VP0R": @"Power Supply Voltage",
|
|
@"IC0C": @"CPU Core Current",
|
|
@"ID0R": @"DC-In Rail Current",
|
|
@"IG0C": @"GPU Rail Current",
|
|
@"IM0R": @"Memory Rail Current"
|
|
};
|
|
});
|
|
|
|
NSString *match = names[key];
|
|
return match ?: key;
|
|
}
|
|
|
|
+ (NSDictionary<NSString *, id> *)calculatePowerMetricsWithReadings:(NSDictionary<NSString *, NSNumber *> *)readings
|
|
batteryInfo:(nullable NSDictionary<NSString *, id> *)batteryInfo {
|
|
// Determine system total power
|
|
double systemTotal = 0.0;
|
|
if (readings[@"PSTR"]) {
|
|
systemTotal = [readings[@"PSTR"] doubleValue];
|
|
} else if (readings[@"PDTR"]) {
|
|
systemTotal = [readings[@"PDTR"] doubleValue];
|
|
} else if (batteryInfo[@"batteryWatts"]) {
|
|
systemTotal = [batteryInfo[@"batteryWatts"] doubleValue];
|
|
}
|
|
|
|
// CPU wattage
|
|
double cpuWatts = 0.0;
|
|
if (readings[@"PCPR"]) {
|
|
cpuWatts = [readings[@"PCPR"] doubleValue];
|
|
} else if (readings[@"PCTR"]) {
|
|
cpuWatts = [readings[@"PCTR"] doubleValue];
|
|
} else if (readings[@"PC0C"]) {
|
|
cpuWatts = [readings[@"PC0C"] doubleValue];
|
|
} else if (readings[@"PCPT"]) {
|
|
cpuWatts = [readings[@"PCPT"] doubleValue];
|
|
}
|
|
|
|
// Voltage & Current
|
|
double cpuVoltage = readings[@"VC0C"] ? [readings[@"VC0C"] doubleValue] : 0.0;
|
|
double cpuCurrent = readings[@"IC0C"] ? [readings[@"IC0C"] doubleValue] : 0.0;
|
|
|
|
// Fallback: calculate cpuWatts from V * I if SMC didn't provide direct wattage
|
|
if (cpuWatts == 0.0 && cpuVoltage > 0.0 && cpuCurrent > 0.0) {
|
|
cpuWatts = cpuVoltage * cpuCurrent;
|
|
}
|
|
|
|
// GPU wattage
|
|
double gpuWatts = 0.0;
|
|
if (readings[@"PG0R"]) {
|
|
gpuWatts = [readings[@"PG0R"] doubleValue];
|
|
} else if (readings[@"PGTR"]) {
|
|
gpuWatts = [readings[@"PGTR"] doubleValue];
|
|
}
|
|
|
|
// Memory wattage
|
|
double memoryWatts = 0.0;
|
|
if (readings[@"PMSR"]) {
|
|
memoryWatts = [readings[@"PMSR"] doubleValue];
|
|
} else if (readings[@"PM0R"]) {
|
|
memoryWatts = [readings[@"PM0R"] doubleValue];
|
|
}
|
|
|
|
// Collect all individual sensor entries
|
|
NSMutableArray<NSDictionary<NSString *, id> *> *sensors = [NSMutableArray arrayWithCapacity:readings.count];
|
|
NSArray<NSString *> *sortedKeys = [readings.allKeys sortedArrayUsingSelector:@selector(compare:)];
|
|
for (NSString *k in sortedKeys) {
|
|
double val = [readings[k] doubleValue];
|
|
[sensors addObject:@{
|
|
@"key": k,
|
|
@"name": [self humanReadableNameForKey:k],
|
|
@"value": @(val),
|
|
@"unit": [self unitForKey:k]
|
|
}];
|
|
}
|
|
|
|
NSString *powerSource = batteryInfo[@"powerSource"] ?: @"AC Power";
|
|
BOOL isCharging = [batteryInfo[@"isCharging"] boolValue];
|
|
double batteryLevel = [batteryInfo[@"batteryLevel"] doubleValue];
|
|
|
|
return @{
|
|
@"systemTotalWatts": @(systemTotal),
|
|
@"cpuWatts": @(cpuWatts),
|
|
@"gpuWatts": @(gpuWatts),
|
|
@"memoryWatts": @(memoryWatts),
|
|
@"cpuVoltage": @(cpuVoltage),
|
|
@"cpuCurrent": @(cpuCurrent),
|
|
@"powerSource": powerSource,
|
|
@"isCharging": @(isCharging),
|
|
@"batteryLevel": @(batteryLevel),
|
|
@"sensors": sensors
|
|
};
|
|
}
|
|
|
|
- (nullable NSDictionary<NSString *, id> *)sampleBatteryInfo {
|
|
CFTypeRef psInfo = IOPSCopyPowerSourcesInfo();
|
|
if (!psInfo) return nil;
|
|
|
|
CFArrayRef psList = IOPSCopyPowerSourcesList(psInfo);
|
|
if (!psList) {
|
|
CFRelease(psInfo);
|
|
return nil;
|
|
}
|
|
|
|
NSString *stateStr = @"AC Power";
|
|
BOOL isCharging = NO;
|
|
double capacity = 100.0;
|
|
|
|
if (CFArrayGetCount(psList) > 0) {
|
|
CFTypeRef ps = CFArrayGetValueAtIndex(psList, 0);
|
|
CFDictionaryRef desc = IOPSGetPowerSourceDescription(psInfo, ps);
|
|
if (desc) {
|
|
NSDictionary *dict = (__bridge NSDictionary *)desc;
|
|
stateStr = dict[@"Power Source State"] ?: @"AC Power";
|
|
isCharging = [dict[@"Is Charging"] boolValue];
|
|
if (dict[@"Current Capacity"]) {
|
|
capacity = [dict[@"Current Capacity"] doubleValue];
|
|
}
|
|
}
|
|
}
|
|
CFRelease(psList);
|
|
CFRelease(psInfo);
|
|
|
|
// Sample AppleSmartBattery for instantaneous voltage and current
|
|
double batteryWatts = 0.0;
|
|
double batteryVoltage = 0.0;
|
|
double batteryAmperage = 0.0;
|
|
|
|
io_service_t service = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("AppleSmartBattery"));
|
|
if (service != IO_OBJECT_NULL) {
|
|
CFMutableDictionaryRef props = NULL;
|
|
if (IORegistryEntryCreateCFProperties(service, &props, kCFAllocatorDefault, 0) == KERN_SUCCESS && props) {
|
|
NSDictionary *dict = (__bridge NSDictionary *)props;
|
|
double v_mV = [dict[@"Voltage"] doubleValue];
|
|
double a_mA = dict[@"InstantAmperage"] ? [dict[@"InstantAmperage"] doubleValue] : [dict[@"Amperage"] doubleValue];
|
|
|
|
batteryVoltage = v_mV / 1000.0;
|
|
batteryAmperage = fabs(a_mA) / 1000.0;
|
|
batteryWatts = batteryVoltage * batteryAmperage;
|
|
|
|
CFRelease(props);
|
|
}
|
|
IOObjectRelease(service);
|
|
}
|
|
|
|
return @{
|
|
@"powerSource": stateStr,
|
|
@"isCharging": @(isCharging),
|
|
@"batteryLevel": @(capacity),
|
|
@"batteryVoltage": @(batteryVoltage),
|
|
@"batteryAmperage": @(batteryAmperage),
|
|
@"batteryWatts": @(batteryWatts)
|
|
};
|
|
}
|
|
|
|
- (nullable NSDictionary<NSString *, id> *)sampleTelemetryWithError:(NSError **)error {
|
|
if (!_probed && _smcClient.isAvailable) {
|
|
[self probeSensors];
|
|
}
|
|
|
|
NSMutableDictionary<NSString *, NSNumber *> *readings = [NSMutableDictionary dictionary];
|
|
|
|
NSMutableArray<NSString *> *allKeys = [NSMutableArray arrayWithArray:_discoveredPowerKeys];
|
|
[allKeys addObjectsFromArray:_discoveredVoltageKeys];
|
|
[allKeys addObjectsFromArray:_discoveredCurrentKeys];
|
|
|
|
for (NSString *key in allKeys) {
|
|
NSNumber *val = [_smcClient readNumericValueForKey:key error:nil];
|
|
if (val) {
|
|
double v = [val doubleValue];
|
|
// Normalize mV or mA if values appear raw
|
|
if ([key hasPrefix:@"V"] && v > 100.0) {
|
|
v /= 1000.0;
|
|
} else if ([key hasPrefix:@"I"] && v > 500.0) {
|
|
v /= 1000.0;
|
|
}
|
|
readings[key] = @(v);
|
|
}
|
|
}
|
|
|
|
NSDictionary<NSString *, id> *batteryInfo = [self sampleBatteryInfo];
|
|
return [MMPowerTelemetryProvider calculatePowerMetricsWithReadings:readings
|
|
batteryInfo:batteryInfo];
|
|
}
|
|
|
|
@end
|