Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fda8bf9e49 | ||
|
|
feceb2412d |
@@ -17,5 +17,6 @@
|
|||||||
#import "MMStorageTelemetryProvider.h"
|
#import "MMStorageTelemetryProvider.h"
|
||||||
#import "MMCPUThermalProvider.h"
|
#import "MMCPUThermalProvider.h"
|
||||||
#import "MMFanTelemetryProvider.h"
|
#import "MMFanTelemetryProvider.h"
|
||||||
|
#import "MMComponentThermalProvider.h"
|
||||||
|
|
||||||
#endif /* MacMonitor_Bridging_Header_h */
|
#endif /* MacMonitor_Bridging_Header_h */
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#ifndef MMComponentThermalProvider_h
|
||||||
|
#define MMComponentThermalProvider_h
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import "MMTelemetryProvider.h"
|
||||||
|
#import "MMAppleSMCClient.h"
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MMComponentThermalProvider
|
||||||
|
* Samples motherboard, PCH chipset, heatsink, memory, and enclosure ambient temperatures
|
||||||
|
* via AppleSMC.
|
||||||
|
*/
|
||||||
|
@interface MMComponentThermalProvider : NSObject <MMTelemetryProvider>
|
||||||
|
|
||||||
|
@property (nonatomic, strong, readonly) MMAppleSMCClient *smcClient;
|
||||||
|
@property (nonatomic, readonly) NSArray<NSString *> *discoveredKeys;
|
||||||
|
|
||||||
|
- (instancetype)init;
|
||||||
|
- (instancetype)initWithSMCClient:(MMAppleSMCClient *)client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Categorize SMC temperature key into component domain (e.g., PCH / Chipset, Heatsink, Memory, etc.)
|
||||||
|
*/
|
||||||
|
+ (NSString *)categoryForKey:(NSString *)key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Human-readable friendly name for known component SMC keys.
|
||||||
|
*/
|
||||||
|
+ (NSString *)humanReadableNameForKey:(NSString *)key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static calculator method for deterministic unit testing.
|
||||||
|
*/
|
||||||
|
+ (NSDictionary<NSString *, id> *)calculateComponentMetricsFromDictionary:(NSDictionary<NSString *, NSNumber *> *)readings;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
|
||||||
|
#endif /* MMComponentThermalProvider_h */
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
#import "MMComponentThermalProvider.h"
|
||||||
|
|
||||||
|
@interface MMComponentThermalProvider () {
|
||||||
|
MMAppleSMCClient *_smcClient;
|
||||||
|
NSMutableArray<NSString *> *_discoveredKeys;
|
||||||
|
BOOL _probed;
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation MMComponentThermalProvider
|
||||||
|
|
||||||
|
- (instancetype)init {
|
||||||
|
return [self initWithSMCClient:[MMAppleSMCClient sharedClient]];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)initWithSMCClient:(MMAppleSMCClient *)client {
|
||||||
|
self = [super init];
|
||||||
|
if (self) {
|
||||||
|
_smcClient = client;
|
||||||
|
_discoveredKeys = [NSMutableArray array];
|
||||||
|
[self probeSensors];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)probeSensors {
|
||||||
|
if (!_smcClient.isAvailable) return;
|
||||||
|
|
||||||
|
NSArray<NSString *> *candidates = @[
|
||||||
|
@"TPCD", @"TP0P", @"TP0D",
|
||||||
|
@"Th0H", @"Th1H", @"Th2H", @"Th3H",
|
||||||
|
@"TM0P", @"TM1P", @"TM2P", @"TM3P", @"TM0S", @"TM1S",
|
||||||
|
@"Tp0P", @"Tp1P", @"Tp2P", @"Tp0C",
|
||||||
|
@"TA0P", @"TA1P", @"TA0S", @"TA1S",
|
||||||
|
@"TTLD", @"TT1D", @"TB0T", @"TB1T"
|
||||||
|
];
|
||||||
|
|
||||||
|
[_discoveredKeys removeAllObjects];
|
||||||
|
for (NSString *key in candidates) {
|
||||||
|
NSNumber *val = [_smcClient readNumericValueForKey:key error:nil];
|
||||||
|
if (val && [val doubleValue] > 0.0 && [val doubleValue] < 125.0) {
|
||||||
|
[_discoveredKeys addObject:key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_probed = YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (MMTelemetryDomain)domain {
|
||||||
|
return MMTelemetryDomainThermal;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)providerIdentifier {
|
||||||
|
return @"com.i3omb.macmonitor.telemetry.thermal.components";
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)isAvailable {
|
||||||
|
return _smcClient.isAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (MMAppleSMCClient *)smcClient {
|
||||||
|
return _smcClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSArray<NSString *> *)discoveredKeys {
|
||||||
|
return [_discoveredKeys copy];
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (NSString *)categoryForKey:(NSString *)key {
|
||||||
|
if ([key hasPrefix:@"TP"]) return @"PCH / Chipset";
|
||||||
|
if ([key hasPrefix:@"Th"]) return @"Heatsink";
|
||||||
|
if ([key hasPrefix:@"TM"]) return @"Memory";
|
||||||
|
if ([key hasPrefix:@"Tp"]) return @"Power Supply";
|
||||||
|
if ([key hasPrefix:@"TA"]) return @"Ambient / Enclosure";
|
||||||
|
if ([key hasPrefix:@"TT"] || [key hasPrefix:@"TB"] || [key hasPrefix:@"TI"]) return @"Thunderbolt / IO";
|
||||||
|
return @"Other";
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (NSString *)humanReadableNameForKey:(NSString *)key {
|
||||||
|
static NSDictionary<NSString *, NSString *> *names = nil;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
names = @{
|
||||||
|
@"TPCD": @"Platform Controller Hub (Die)",
|
||||||
|
@"TP0P": @"Platform Controller Hub (Proximity)",
|
||||||
|
@"TP0D": @"Platform Controller Hub (Die Alternate)",
|
||||||
|
@"Th0H": @"Main Heatsink A",
|
||||||
|
@"Th1H": @"Main Heatsink B",
|
||||||
|
@"Th2H": @"Main Heatsink C",
|
||||||
|
@"Th3H": @"Main Heatsink D",
|
||||||
|
@"TM0P": @"Memory Bank 0 Proximity",
|
||||||
|
@"TM1P": @"Memory Bank 1 Proximity",
|
||||||
|
@"TM2P": @"Memory Bank 2 Proximity",
|
||||||
|
@"TM3P": @"Memory Bank 3 Proximity",
|
||||||
|
@"TM0S": @"Memory Module 0",
|
||||||
|
@"TM1S": @"Memory Module 1",
|
||||||
|
@"Tp0P": @"Power Supply Primary",
|
||||||
|
@"Tp1P": @"Power Supply Secondary",
|
||||||
|
@"Tp2P": @"Power Supply Tertiary",
|
||||||
|
@"Tp0C": @"Power Supply Core",
|
||||||
|
@"TA0P": @"Ambient Air Intake",
|
||||||
|
@"TA1P": @"Ambient Air Exhaust",
|
||||||
|
@"TA0S": @"Ambient Air Enclosure",
|
||||||
|
@"TA1S": @"Ambient Air Secondary",
|
||||||
|
@"TTLD": @"Thunderbolt Controller Die",
|
||||||
|
@"TT1D": @"Thunderbolt Controller Secondary",
|
||||||
|
@"TB0T": @"Thunderbolt Port 0",
|
||||||
|
@"TB1T": @"Thunderbolt Port 1"
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
NSString *match = names[key];
|
||||||
|
return match ?: key;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (NSDictionary<NSString *, id> *)calculateComponentMetricsFromDictionary:(NSDictionary<NSString *, NSNumber *> *)readings {
|
||||||
|
NSMutableArray<NSDictionary<NSString *, id> *> *components = [NSMutableArray arrayWithCapacity:readings.count];
|
||||||
|
NSMutableDictionary<NSString *, NSMutableArray<NSDictionary<NSString *, id> *> *> *categorized = [NSMutableDictionary dictionary];
|
||||||
|
|
||||||
|
NSArray<NSString *> *sortedKeys = [readings.allKeys sortedArrayUsingSelector:@selector(compare:)];
|
||||||
|
double peak = 0.0;
|
||||||
|
double sum = 0.0;
|
||||||
|
|
||||||
|
for (NSString *k in sortedKeys) {
|
||||||
|
double temp = [readings[k] doubleValue];
|
||||||
|
if (temp > peak) peak = temp;
|
||||||
|
sum += temp;
|
||||||
|
|
||||||
|
NSString *cat = [self categoryForKey:k];
|
||||||
|
NSString *name = [self humanReadableNameForKey:k];
|
||||||
|
|
||||||
|
NSDictionary<NSString *, id> *entry = @{
|
||||||
|
@"key": k,
|
||||||
|
@"name": name,
|
||||||
|
@"category": cat,
|
||||||
|
@"temperature": @(temp)
|
||||||
|
};
|
||||||
|
|
||||||
|
[components addObject:entry];
|
||||||
|
|
||||||
|
if (!categorized[cat]) {
|
||||||
|
categorized[cat] = [NSMutableArray array];
|
||||||
|
}
|
||||||
|
[categorized[cat] addObject:entry];
|
||||||
|
}
|
||||||
|
|
||||||
|
double avg = readings.count > 0 ? (sum / (double)readings.count) : 0.0;
|
||||||
|
|
||||||
|
return @{
|
||||||
|
@"components": components,
|
||||||
|
@"categorized": categorized,
|
||||||
|
@"sensorCount": @(readings.count),
|
||||||
|
@"peakTemperature": @(peak),
|
||||||
|
@"averageTemperature": @(avg)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
- (nullable NSDictionary<NSString *, id> *)sampleTelemetryWithError:(NSError **)error {
|
||||||
|
if (!_probed && _smcClient.isAvailable) {
|
||||||
|
[self probeSensors];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableDictionary<NSString *, NSNumber *> *readings = [NSMutableDictionary dictionary];
|
||||||
|
for (NSString *key in _discoveredKeys) {
|
||||||
|
NSNumber *temp = [_smcClient readNumericValueForKey:key error:nil];
|
||||||
|
if (temp) {
|
||||||
|
readings[key] = temp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [MMComponentThermalProvider calculateComponentMetricsFromDictionary:readings];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import MacMonitor
|
||||||
|
|
||||||
|
final class MMComponentThermalTests: XCTestCase {
|
||||||
|
|
||||||
|
func testCategoryClassification() {
|
||||||
|
XCTAssertEqual(MMComponentThermalProvider.category(forKey: "TPCD"), "PCH / Chipset")
|
||||||
|
XCTAssertEqual(MMComponentThermalProvider.category(forKey: "TP0P"), "PCH / Chipset")
|
||||||
|
XCTAssertEqual(MMComponentThermalProvider.category(forKey: "Th0H"), "Heatsink")
|
||||||
|
XCTAssertEqual(MMComponentThermalProvider.category(forKey: "Th1H"), "Heatsink")
|
||||||
|
XCTAssertEqual(MMComponentThermalProvider.category(forKey: "TM0P"), "Memory")
|
||||||
|
XCTAssertEqual(MMComponentThermalProvider.category(forKey: "TM1S"), "Memory")
|
||||||
|
XCTAssertEqual(MMComponentThermalProvider.category(forKey: "Tp0P"), "Power Supply")
|
||||||
|
XCTAssertEqual(MMComponentThermalProvider.category(forKey: "TA0P"), "Ambient / Enclosure")
|
||||||
|
XCTAssertEqual(MMComponentThermalProvider.category(forKey: "TTLD"), "Thunderbolt / IO")
|
||||||
|
XCTAssertEqual(MMComponentThermalProvider.category(forKey: "TB0T"), "Thunderbolt / IO")
|
||||||
|
XCTAssertEqual(MMComponentThermalProvider.category(forKey: "ZZ99"), "Other")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHumanReadableNames() {
|
||||||
|
XCTAssertEqual(MMComponentThermalProvider.humanReadableName(forKey: "TPCD"), "Platform Controller Hub (Die)")
|
||||||
|
XCTAssertEqual(MMComponentThermalProvider.humanReadableName(forKey: "Th0H"), "Main Heatsink A")
|
||||||
|
XCTAssertEqual(MMComponentThermalProvider.humanReadableName(forKey: "TA0P"), "Ambient Air Intake")
|
||||||
|
XCTAssertEqual(MMComponentThermalProvider.humanReadableName(forKey: "CUSTOM"), "CUSTOM")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testComponentMetricsCalculation() {
|
||||||
|
let readings: [String: NSNumber] = [
|
||||||
|
"TPCD": NSNumber(value: 65.5),
|
||||||
|
"Th0H": NSNumber(value: 48.0),
|
||||||
|
"TM0P": NSNumber(value: 42.0),
|
||||||
|
"TA0P": NSNumber(value: 28.5)
|
||||||
|
]
|
||||||
|
|
||||||
|
let metrics = MMComponentThermalProvider.calculateComponentMetrics(from: readings)
|
||||||
|
|
||||||
|
let peak = metrics["peakTemperature"] as? Double ?? 0
|
||||||
|
let avg = metrics["averageTemperature"] as? Double ?? 0
|
||||||
|
let count = metrics["sensorCount"] as? Int ?? 0
|
||||||
|
let components = metrics["components"] as? [[String: Any]] ?? []
|
||||||
|
let categorized = metrics["categorized"] as? [String: [[String: Any]]] ?? [:]
|
||||||
|
|
||||||
|
XCTAssertEqual(peak, 65.5, accuracy: 0.1)
|
||||||
|
XCTAssertEqual(avg, (65.5 + 48.0 + 42.0 + 28.5) / 4.0, accuracy: 0.1)
|
||||||
|
XCTAssertEqual(count, 4)
|
||||||
|
XCTAssertEqual(components.count, 4)
|
||||||
|
|
||||||
|
let pchList = categorized["PCH / Chipset"]
|
||||||
|
XCTAssertNotNil(pchList)
|
||||||
|
let firstPchName = pchList?.first?["name"] as? String
|
||||||
|
XCTAssertEqual(firstPchName, "Platform Controller Hub (Die)")
|
||||||
|
XCTAssertEqual(categorized["Heatsink"]?.count, 1)
|
||||||
|
XCTAssertEqual(categorized["Memory"]?.count, 1)
|
||||||
|
XCTAssertEqual(categorized["Ambient / Enclosure"]?.count, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEmptyReadingsCalculation() {
|
||||||
|
let metrics = MMComponentThermalProvider.calculateComponentMetrics(from: [:])
|
||||||
|
|
||||||
|
let peak = metrics["peakTemperature"] as? Double ?? -1
|
||||||
|
let avg = metrics["averageTemperature"] as? Double ?? -1
|
||||||
|
let count = metrics["sensorCount"] as? Int ?? -1
|
||||||
|
|
||||||
|
XCTAssertEqual(peak, 0.0)
|
||||||
|
XCTAssertEqual(avg, 0.0)
|
||||||
|
XCTAssertEqual(count, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLiveComponentThermalProvider() throws {
|
||||||
|
let provider = MMComponentThermalProvider()
|
||||||
|
XCTAssertEqual(provider.domain, .thermal)
|
||||||
|
XCTAssertEqual(provider.providerIdentifier, "com.i3omb.macmonitor.telemetry.thermal.components")
|
||||||
|
|
||||||
|
let sample = try provider.sampleTelemetry()
|
||||||
|
XCTAssertNotNil(sample)
|
||||||
|
XCTAssertNotNil(sample["sensorCount"])
|
||||||
|
XCTAssertNotNil(sample["components"])
|
||||||
|
XCTAssertNotNil(sample["categorized"])
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user