Files
MacMonitor/Sources/Telemetry/Power/MMPowerTelemetryProvider.m
gronod 91d50dcbae
MacMonitor CI/CD Pipeline / Build & Test (Intel x86_64) (push) Canceled after 0s
feat(telemetry): implement power, voltage, and current telemetry provider (fixes #19)
2026-09-08 12:25:34 +01:00

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