Files
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 4a8fd0e5ef fix(telemetry): realign snapshot contract — provider IDs and dictionary field keys (fixes #30, fixes #31)
Providers emitted identifiers and field keys that decodeSnapshot never
looked up, leaving CPU/kernel/load/disk sections zeroed and memory%,
storage%, network, process RSS, and fan fields decoded as zero.

Store-side decode now reads the emitted schema; providers gain the
previously-missing fields: network isUp (IFF_UP via getifaddrs), process
isStopped/isZombie (pbi_status), fan name (F{i}ID). Memory utilization is
computed as used/total in the decoder. decodeSnapshot is internal for
@testable access; new MMSnapshotContractTests cover the emitted-key
contract with synthetic dictionaries (93 tests total, 0 failures).

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-08 15:09:19 +01:00

160 lines
5.1 KiB
Objective-C

#import "MMFanTelemetryProvider.h"
@interface MMFanTelemetryProvider () {
MMAppleSMCClient *_smcClient;
NSInteger _probedFanCount;
}
@end
@implementation MMFanTelemetryProvider
- (instancetype)init {
return [self initWithSMCClient:[MMAppleSMCClient sharedClient]];
}
- (instancetype)initWithSMCClient:(MMAppleSMCClient *)client {
self = [super init];
if (self) {
_smcClient = client;
_probedFanCount = -1;
[self probeFans];
}
return self;
}
- (void)probeFans {
if (!_smcClient.isAvailable) return;
NSNumber *num = [_smcClient readNumericValueForKey:@"FNum" error:nil];
if (num) {
_probedFanCount = [num integerValue];
} else {
// Fallback probe F0Ac
NSNumber *f0 = [_smcClient readNumericValueForKey:@"F0Ac" error:nil];
if (f0 && [f0 doubleValue] >= 0.0) {
_probedFanCount = 1;
NSNumber *f1 = [_smcClient readNumericValueForKey:@"F1Ac" error:nil];
if (f1 && [f1 doubleValue] >= 0.0) {
_probedFanCount = 2;
}
} else {
_probedFanCount = 0;
}
}
}
- (MMTelemetryDomain)domain {
return MMTelemetryDomainFan;
}
- (NSString *)providerIdentifier {
return @"com.i3omb.macmonitor.telemetry.fan";
}
- (BOOL)isAvailable {
return _smcClient.isAvailable;
}
- (MMAppleSMCClient *)smcClient {
return _smcClient;
}
- (NSInteger)fanCount {
return (_probedFanCount >= 0) ? _probedFanCount : 0;
}
+ (NSDictionary<NSString *, id> *)calculateFanMetricsFromFanList:(NSArray<NSDictionary<NSString *, id> *> *)fanList {
if (!fanList || fanList.count == 0) {
return @{
@"fanCount": @(0),
@"fans": @[],
@"averageRPM": @(0.0),
@"peakRPM": @(0.0)
};
}
NSMutableArray<NSDictionary<NSString *, id> *> *resultList = [NSMutableArray arrayWithCapacity:fanList.count];
double sumRPM = 0.0;
double peakRPM = 0.0;
for (NSUInteger i = 0; i < fanList.count; i++) {
NSDictionary<NSString *, id> *raw = fanList[i];
double current = [raw[@"currentRPM"] doubleValue];
double min = [raw[@"minRPM"] doubleValue];
double max = [raw[@"maxRPM"] doubleValue];
double target = [raw[@"targetRPM"] doubleValue];
double util = 0.0;
if (max > min && current >= min) {
util = ((current - min) / (max - min)) * 100.0;
if (util < 0.0) util = 0.0;
if (util > 100.0) util = 100.0;
}
if (current > peakRPM) peakRPM = current;
sumRPM += current;
[resultList addObject:@{
@"fanIndex": @(i),
@"name": raw[@"name"] ?: [NSString stringWithFormat:@"Fan %lu", (unsigned long)(i + 1)],
@"currentRPM": @(current),
@"minRPM": @(min),
@"maxRPM": @(max),
@"targetRPM": @(target),
@"utilizationPercent": @(util)
}];
}
double avgRPM = fanList.count > 0 ? (sumRPM / fanList.count) : 0.0;
return @{
@"fanCount": @(fanList.count),
@"fans": [resultList copy],
@"averageRPM": @(avgRPM),
@"peakRPM": @(peakRPM)
};
}
- (nullable NSDictionary<NSString *, id> *)sampleTelemetryWithError:(NSError **)error {
if (_probedFanCount < 0 && _smcClient.isAvailable) {
[self probeFans];
}
NSInteger count = [self fanCount];
NSMutableArray<NSDictionary<NSString *, id> *> *rawList = [NSMutableArray arrayWithCapacity:count];
for (NSInteger i = 0; i < count; i++) {
NSString *acKey = [NSString stringWithFormat:@"F%ldAc", (long)i];
NSString *mnKey = [NSString stringWithFormat:@"F%ldMn", (long)i];
NSString *mxKey = [NSString stringWithFormat:@"F%ldMx", (long)i];
NSString *tgKey = [NSString stringWithFormat:@"F%ldTg", (long)i];
NSString *idKey = [NSString stringWithFormat:@"F%ldID", (long)i];
NSNumber *ac = [_smcClient readNumericValueForKey:acKey error:nil] ?: @(0.0);
NSNumber *mn = [_smcClient readNumericValueForKey:mnKey error:nil] ?: @(0.0);
NSNumber *mx = [_smcClient readNumericValueForKey:mxKey error:nil] ?: @(6000.0);
NSNumber *tg = [_smcClient readNumericValueForKey:tgKey error:nil] ?: ac;
NSString *fanName = [NSString stringWithFormat:@"Fan %ld", (long)(i + 1)];
NSData *idData = [_smcClient readBytesForKey:idKey error:nil];
if (idData.length > 0) {
NSString *raw = [[NSString alloc] initWithData:idData encoding:NSASCIIStringEncoding];
NSString *trimmed = [raw stringByTrimmingCharactersInSet:
[NSCharacterSet characterSetWithCharactersInString:@"\0 "]];
if (trimmed.length > 0) fanName = trimmed;
}
[rawList addObject:@{
@"name": fanName,
@"currentRPM": ac,
@"minRPM": mn,
@"maxRPM": mx,
@"targetRPM": tg
}];
}
return [MMFanTelemetryProvider calculateFanMetricsFromFanList:rawList];
}
@end