Show the CPU temperature on Apple Silicon

Power & Storage only had macOS's thermal state (a level, no degrees) and
the battery temperature. The CPU die sensors are now read through
IOHIDEventSystemClient (no root or entitlement needed outside the
sandbox): "PMU tdie*" from M2 on, "pACC/eACC MTR Temp Sensor*" on M1.
System Status shows the hottest one, with the average and sensor count in
the tooltip, coloured from 80/90/100 °C. Intel Macs have no such sensors
and the row stays hidden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
hariel1985
2026-10-05 21:51:42 +02:00
szülő 2685ed9667
commit 15c8b18b48
6 fájl változott, egészen pontosan 240 új sor hozzáadva és 3 régi sor törölve

Fájl megtekintése

@@ -0,0 +1,71 @@
import XCTest
@testable import TopManager
final class ThermalMathTests: XCTestCase {
// Names as an M3 reports them (PMU / PMU2 dies, package, calibration, NAND, battery).
private let m3: [TemperatureReading] = [
.init(name: "PMU tdie1", celsius: 48.8),
.init(name: "PMU tdie2", celsius: 46.4),
.init(name: "PMU2 tdie1", celsius: 43.4),
.init(name: "PMU tdev2", celsius: 36.2),
.init(name: "PMU tdev1", celsius: -1.5),
.init(name: "PMU tcal", celsius: 51.9),
.init(name: "NAND CH0 temp", celsius: 34.0),
.init(name: "gas gauge battery", celsius: 31.3),
]
func testOnlyDieSensorsCount() {
let info = ThermalMath.summarize(m3)
XCTAssertEqual(info?.cpuSensorCount, 3)
// tcal (51.9) is hotter than every die but is a calibration value, not the CPU.
XCTAssertEqual(info?.cpuMax ?? 0, 48.8, accuracy: 0.001)
XCTAssertEqual(info?.cpuAverage ?? 0, (48.8 + 46.4 + 43.4) / 3, accuracy: 0.001)
}
func testM1ClusterSensorsCount() {
let m1: [TemperatureReading] = [
.init(name: "pACC MTR Temp Sensor0", celsius: 55),
.init(name: "eACC MTR Temp Sensor1", celsius: 41),
.init(name: "GPU MTR Temp Sensor1", celsius: 60),
.init(name: "SOC MTR Temp Sensor0", celsius: 50),
]
let info = ThermalMath.summarize(m1)
XCTAssertEqual(info?.cpuSensorCount, 2)
XCTAssertEqual(info?.cpuMax ?? 0, 55, accuracy: 0.001)
}
func testImplausibleDieReadingsAreDropped() {
let readings: [TemperatureReading] = [
.init(name: "PMU tdie1", celsius: 0),
.init(name: "PMU tdie2", celsius: -2),
.init(name: "PMU tdie3", celsius: 170),
.init(name: "PMU tdie4", celsius: 44),
]
XCTAssertEqual(ThermalMath.summarize(readings)?.cpuSensorCount, 1)
}
func testNoCPUSensorsMeansNil() {
// An Intel Mac: no IOHID die sensors at all, or only non-CPU ones.
XCTAssertNil(ThermalMath.summarize([]))
XCTAssertNil(ThermalMath.summarize([.init(name: "gas gauge battery", celsius: 30)]))
}
func testReadsRealSensorsOnAppleSilicon() throws {
#if !arch(arm64)
throw XCTSkip("Apple Silicon only: Intel Macs expose these sensors through the SMC")
#else
let info = try XCTUnwrap(TemperatureMonitor().fetchThermalInfo())
XCTAssertGreaterThan(info.cpuSensorCount, 0)
XCTAssertTrue((15...110).contains(info.cpuMax), "implausible CPU temperature \(info.cpuMax)")
XCTAssertLessThanOrEqual(info.cpuAverage, info.cpuMax)
#endif
}
func testLevels() {
XCTAssertEqual(ThermalMath.level(cpuCelsius: 48), .nominal)
XCTAssertEqual(ThermalMath.level(cpuCelsius: 85), .fair)
XCTAssertEqual(ThermalMath.level(cpuCelsius: 95), .serious)
XCTAssertEqual(ThermalMath.level(cpuCelsius: 104), .critical)
}
}