Compare commits
2
Commits
36c7ea2319
...
d8e469ec9e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8e469ec9e | ||
|
|
91d50dcbae |
@@ -18,5 +18,6 @@
|
|||||||
#import "MMCPUThermalProvider.h"
|
#import "MMCPUThermalProvider.h"
|
||||||
#import "MMFanTelemetryProvider.h"
|
#import "MMFanTelemetryProvider.h"
|
||||||
#import "MMComponentThermalProvider.h"
|
#import "MMComponentThermalProvider.h"
|
||||||
|
#import "MMPowerTelemetryProvider.h"
|
||||||
|
|
||||||
#endif /* MacMonitor_Bridging_Header_h */
|
#endif /* MacMonitor_Bridging_Header_h */
|
||||||
|
|||||||
@@ -40,6 +40,13 @@
|
|||||||
(uint32_t)bytes[3];
|
(uint32_t)bytes[3];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int hexDigitValue(unichar c) {
|
||||||
|
if (c >= '0' && c <= '9') return c - '0';
|
||||||
|
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||||
|
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
+ (nullable NSNumber *)decodeValueWithDataType:(NSString *)dataType bytes:(const uint8_t *)bytes size:(NSUInteger)size {
|
+ (nullable NSNumber *)decodeValueWithDataType:(NSString *)dataType bytes:(const uint8_t *)bytes size:(NSUInteger)size {
|
||||||
if (!bytes || size == 0) return nil;
|
if (!bytes || size == 0) return nil;
|
||||||
|
|
||||||
@@ -49,7 +56,7 @@
|
|||||||
if (size >= 2) return @([self decodeSP78:bytes]);
|
if (size >= 2) return @([self decodeSP78:bytes]);
|
||||||
} else if ([trimmedType isEqualToString:@"fpe2"]) {
|
} else if ([trimmedType isEqualToString:@"fpe2"]) {
|
||||||
if (size >= 2) return @([self decodeFPE2:bytes]);
|
if (size >= 2) return @([self decodeFPE2:bytes]);
|
||||||
} else if ([trimmedType isEqualToString:@"flt"]) {
|
} else if ([trimmedType isEqualToString:@"flt"] || [trimmedType isEqualToString:@"ioft"]) {
|
||||||
if (size >= 4) return @([self decodeFLT:bytes]);
|
if (size >= 4) return @([self decodeFLT:bytes]);
|
||||||
} else if ([trimmedType isEqualToString:@"ui8"]) {
|
} else if ([trimmedType isEqualToString:@"ui8"]) {
|
||||||
if (size >= 1) return @([self decodeUI8:bytes]);
|
if (size >= 1) return @([self decodeUI8:bytes]);
|
||||||
@@ -59,6 +66,18 @@
|
|||||||
if (size >= 4) return @([self decodeUI32:bytes]);
|
if (size >= 4) return @([self decodeUI32:bytes]);
|
||||||
} else if ([trimmedType isEqualToString:@"flag"]) {
|
} else if ([trimmedType isEqualToString:@"flag"]) {
|
||||||
if (size >= 1) return @(bytes[0] != 0);
|
if (size >= 1) return @(bytes[0] != 0);
|
||||||
|
} else if (trimmedType.length == 4 && [trimmedType hasPrefix:@"sp"] && size >= 2) {
|
||||||
|
int fracBits = hexDigitValue([trimmedType characterAtIndex:3]);
|
||||||
|
if (fracBits >= 0 && fracBits <= 15) {
|
||||||
|
int16_t raw = (int16_t)(((uint16_t)bytes[0] << 8) | (uint16_t)bytes[1]);
|
||||||
|
return @((float)raw / (float)(1 << fracBits));
|
||||||
|
}
|
||||||
|
} else if (trimmedType.length == 4 && [trimmedType hasPrefix:@"fp"] && size >= 2) {
|
||||||
|
int fracBits = hexDigitValue([trimmedType characterAtIndex:3]);
|
||||||
|
if (fracBits >= 0 && fracBits <= 15) {
|
||||||
|
uint16_t raw = ((uint16_t)bytes[0] << 8) | (uint16_t)bytes[1];
|
||||||
|
return @((float)raw / (float)(1 << fracBits));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil;
|
return nil;
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#ifndef MMPowerTelemetryProvider_h
|
||||||
|
#define MMPowerTelemetryProvider_h
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import "MMTelemetryProvider.h"
|
||||||
|
#import "MMAppleSMCClient.h"
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MMPowerTelemetryProvider
|
||||||
|
* Samples system power draw, CPU/GPU package wattage, rail voltages, and currents
|
||||||
|
* via AppleSMC and AppleSmartBattery IOKit interfaces.
|
||||||
|
*/
|
||||||
|
@interface MMPowerTelemetryProvider : NSObject <MMTelemetryProvider>
|
||||||
|
|
||||||
|
@property (nonatomic, strong, readonly) MMAppleSMCClient *smcClient;
|
||||||
|
@property (nonatomic, readonly) NSArray<NSString *> *discoveredPowerKeys;
|
||||||
|
@property (nonatomic, readonly) NSArray<NSString *> *discoveredVoltageKeys;
|
||||||
|
@property (nonatomic, readonly) NSArray<NSString *> *discoveredCurrentKeys;
|
||||||
|
|
||||||
|
- (instancetype)init;
|
||||||
|
- (instancetype)initWithSMCClient:(MMAppleSMCClient *)client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Friendly name for power, voltage, and current SMC keys.
|
||||||
|
*/
|
||||||
|
+ (NSString *)humanReadableNameForKey:(NSString *)key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit of measurement for SMC key ("W", "V", "A").
|
||||||
|
*/
|
||||||
|
+ (NSString *)unitForKey:(NSString *)key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure calculator method for deterministic unit testing.
|
||||||
|
*/
|
||||||
|
+ (NSDictionary<NSString *, id> *)calculatePowerMetricsWithReadings:(NSDictionary<NSString *, NSNumber *> *)readings
|
||||||
|
batteryInfo:(nullable NSDictionary<NSString *, id> *)batteryInfo;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
|
||||||
|
#endif /* MMPowerTelemetryProvider_h */
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
#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
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import MacMonitor
|
||||||
|
|
||||||
|
final class MMPowerTests: XCTestCase {
|
||||||
|
|
||||||
|
func testUnitsAndNames() {
|
||||||
|
XCTAssertEqual(MMPowerTelemetryProvider.unit(forKey: "PSTR"), "W")
|
||||||
|
XCTAssertEqual(MMPowerTelemetryProvider.unit(forKey: "VC0C"), "V")
|
||||||
|
XCTAssertEqual(MMPowerTelemetryProvider.unit(forKey: "IC0C"), "A")
|
||||||
|
XCTAssertEqual(MMPowerTelemetryProvider.unit(forKey: "UNKNOWN"), "")
|
||||||
|
|
||||||
|
XCTAssertEqual(MMPowerTelemetryProvider.humanReadableName(forKey: "PSTR"), "Total System Power")
|
||||||
|
XCTAssertEqual(MMPowerTelemetryProvider.humanReadableName(forKey: "VC0C"), "CPU Core Voltage")
|
||||||
|
XCTAssertEqual(MMPowerTelemetryProvider.humanReadableName(forKey: "IC0C"), "CPU Core Current")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPowerMetricsCalculationFromSMC() {
|
||||||
|
let readings: [String: NSNumber] = [
|
||||||
|
"PSTR": NSNumber(value: 45.2),
|
||||||
|
"PCPR": NSNumber(value: 28.4),
|
||||||
|
"PG0R": NSNumber(value: 8.5),
|
||||||
|
"PMSR": NSNumber(value: 3.1),
|
||||||
|
"VC0C": NSNumber(value: 1.15),
|
||||||
|
"IC0C": NSNumber(value: 24.5)
|
||||||
|
]
|
||||||
|
|
||||||
|
let batteryInfo: [String: Any] = [
|
||||||
|
"powerSource": "AC Power",
|
||||||
|
"isCharging": false,
|
||||||
|
"batteryLevel": 100.0,
|
||||||
|
"batteryWatts": 0.0
|
||||||
|
]
|
||||||
|
|
||||||
|
let metrics = MMPowerTelemetryProvider.calculatePowerMetrics(
|
||||||
|
withReadings: readings,
|
||||||
|
batteryInfo: batteryInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
let systemWatts = metrics["systemTotalWatts"] as? Double ?? 0
|
||||||
|
let cpuWatts = metrics["cpuWatts"] as? Double ?? 0
|
||||||
|
let gpuWatts = metrics["gpuWatts"] as? Double ?? 0
|
||||||
|
let memWatts = metrics["memoryWatts"] as? Double ?? 0
|
||||||
|
let cpuV = metrics["cpuVoltage"] as? Double ?? 0
|
||||||
|
let cpuI = metrics["cpuCurrent"] as? Double ?? 0
|
||||||
|
let powerSource = metrics["powerSource"] as? String ?? ""
|
||||||
|
let sensors = metrics["sensors"] as? [[String: Any]] ?? []
|
||||||
|
|
||||||
|
XCTAssertEqual(systemWatts, 45.2, accuracy: 0.1)
|
||||||
|
XCTAssertEqual(cpuWatts, 28.4, accuracy: 0.1)
|
||||||
|
XCTAssertEqual(gpuWatts, 8.5, accuracy: 0.1)
|
||||||
|
XCTAssertEqual(memWatts, 3.1, accuracy: 0.1)
|
||||||
|
XCTAssertEqual(cpuV, 1.15, accuracy: 0.01)
|
||||||
|
XCTAssertEqual(cpuI, 24.5, accuracy: 0.1)
|
||||||
|
XCTAssertEqual(powerSource, "AC Power")
|
||||||
|
XCTAssertEqual(sensors.count, 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBatteryFallbackCalculation() {
|
||||||
|
// No PSTR, no PCPR, but VC0C and IC0C are present
|
||||||
|
let readings: [String: NSNumber] = [
|
||||||
|
"VC0C": NSNumber(value: 1.2),
|
||||||
|
"IC0C": NSNumber(value: 10.0)
|
||||||
|
]
|
||||||
|
|
||||||
|
let batteryInfo: [String: Any] = [
|
||||||
|
"powerSource": "Battery Power",
|
||||||
|
"isCharging": false,
|
||||||
|
"batteryLevel": 75.0,
|
||||||
|
"batteryWatts": 18.5
|
||||||
|
]
|
||||||
|
|
||||||
|
let metrics = MMPowerTelemetryProvider.calculatePowerMetrics(
|
||||||
|
withReadings: readings,
|
||||||
|
batteryInfo: batteryInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
let systemWatts = metrics["systemTotalWatts"] as? Double ?? 0
|
||||||
|
let cpuWatts = metrics["cpuWatts"] as? Double ?? 0
|
||||||
|
let powerSource = metrics["powerSource"] as? String ?? ""
|
||||||
|
let isCharging = metrics["isCharging"] as? Bool ?? true
|
||||||
|
|
||||||
|
// System watts falls back to batteryWatts (18.5)
|
||||||
|
XCTAssertEqual(systemWatts, 18.5, accuracy: 0.1)
|
||||||
|
// CPU watts calculated from V * I = 1.2 * 10.0 = 12.0 W
|
||||||
|
XCTAssertEqual(cpuWatts, 12.0, accuracy: 0.1)
|
||||||
|
XCTAssertEqual(powerSource, "Battery Power")
|
||||||
|
XCTAssertFalse(isCharging)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLivePowerProvider() throws {
|
||||||
|
let provider = MMPowerTelemetryProvider()
|
||||||
|
XCTAssertEqual(provider.domain, .power)
|
||||||
|
XCTAssertEqual(provider.providerIdentifier, "com.i3omb.macmonitor.telemetry.power")
|
||||||
|
|
||||||
|
let sample = try provider.sampleTelemetry()
|
||||||
|
XCTAssertNotNil(sample)
|
||||||
|
XCTAssertNotNil(sample["systemTotalWatts"])
|
||||||
|
XCTAssertNotNil(sample["powerSource"])
|
||||||
|
XCTAssertNotNil(sample["sensors"])
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user