Der Regler war eine Pseudobedienung. Der SMC lehnt Schreibzugriffe auf CHLT ab — der Helfer hat es mit korrekter Größe versucht und „geschrieben (false), zurückgelesen 85" gemeldet. Das Sicherheitsnetz hat also die Wahrheit gesagt; die Oberfläche hat sie nur nicht gezeigt. Weiter zu raten hieße, im Lade-Subsystem Keys durchzuprobieren, und das ist die eine Operation in diesem Projekt, die Hardware dauerhaft beschädigen kann. Also: das Limit wird gelesen und angezeigt, mit einem Weg in die Systemeinstellungen, wo es tatsächlich geht. Der Schreibpfad ist aus dem privilegierten Helfer entfernt — ein Root-Dienst trägt nur, was benutzt wird. Die Akku-Kachel zeigte „40 %" und einen Verlauf über die letzten Sekunden. Der Ladestand ändert sich in Minuten, nicht in Sekunden; der Graph war eine waagerechte Linie. Jetzt: verbleibende Zeit bzw. Zeit bis voll, Leistung in Watt mit Richtung, Ladelimit, Zustand, Zyklen — und der Energiesparmodus. Der erklärt, warum die Maschine langsamer wirkt, und ohne diesen Hinweis sucht man den Grund woanders. In der Menüleiste standen fünf nackte Prozentzahlen nebeneinander. Man sah Zahlen und wusste nicht, welche wozu gehört. Zwei neue Darstellungen: Kürzel und Wert („CPU 34 %") sowie Symbol und Wert. Beim Akku stehen sie zuoberst in der Auswahl. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
484 lines
20 KiB
Swift
484 lines
20 KiB
Swift
import Foundation
|
|
import IOKit
|
|
import IOKit.ps
|
|
import Darwin
|
|
|
|
/// Ein vollständiger Messwertsatz.
|
|
public struct MetricsSnapshot: Equatable, Sendable {
|
|
public var cpu = CPUReading()
|
|
public var gpu: Double?
|
|
/// Wärmster Punkt der GPU in Grad Celsius.
|
|
public var gpuTemperature: Double?
|
|
public var memory = MemoryReading()
|
|
public var battery: BatteryReading?
|
|
public var sensors: [SensorReading] = []
|
|
public var fans: [FanReading] = []
|
|
public var power = PowerReading()
|
|
public var topMemoryProcesses: [ProcessMemory] = []
|
|
public var asOf = Date()
|
|
}
|
|
|
|
/// Wohin die Leistung geht.
|
|
///
|
|
/// Die Schlüssel sind gegen den Aktivitätsmonitor und gegen iStat Menus
|
|
/// abgeglichen: `PHPC` deckt sich mit deren CPU-Wert, `PSTR` mit der
|
|
/// Gesamtleistung.
|
|
public struct PowerReading: Equatable, Sendable {
|
|
public var total: Double?
|
|
public var cpu: Double?
|
|
public var display: Double?
|
|
public var adapter: Double?
|
|
public var busVoltage: Double?
|
|
}
|
|
|
|
public struct CPUReading: Equatable, Sendable {
|
|
/// Gesamtauslastung, 0 bis 1.
|
|
public var total: Double = 0
|
|
/// Auslastung je Kern, in der Reihenfolge des Systems.
|
|
public var perCore: [Double] = []
|
|
/// Leistungsaufnahme in Watt, falls messbar.
|
|
public var watts: Double?
|
|
/// Der wärmste Kern in Grad Celsius.
|
|
///
|
|
/// Apple Silicon hat pro Kern einen Fühler — auf dem M5 Pro sind es
|
|
/// über zwanzig. Der Höchstwert ist der aussagekräftige: gedrosselt wird
|
|
/// nach dem heißesten Punkt, nicht nach dem Durchschnitt.
|
|
public var temperature: Double?
|
|
}
|
|
|
|
public struct MemoryReading: Equatable, Sendable {
|
|
public var used: UInt64 = 0
|
|
public var total: UInt64 = 0
|
|
public var compressed: UInt64 = 0
|
|
public var swapUsed: UInt64 = 0
|
|
public var swapTotal: UInt64 = 0
|
|
/// Von Programmen belegt (aktiv + inaktiv).
|
|
public var app: UInt64 = 0
|
|
/// Vom Kernel festgehalten, nicht auslagerbar.
|
|
public var wired: UInt64 = 0
|
|
public var free: UInt64 = 0
|
|
public var pressure: MemoryPressure = .normal
|
|
|
|
public var usedFraction: Double {
|
|
total > 0 ? Double(used) / Double(total) : 0
|
|
}
|
|
}
|
|
|
|
public struct BatteryReading: Equatable, Sendable {
|
|
public var charge: Double = 0 // 0 bis 1
|
|
public var isCharging = false
|
|
public var isPluggedIn = false
|
|
public var cycleCount: Int?
|
|
public var health: Double? // 0 bis 1
|
|
public var watts: Double? // negativ = Entladung
|
|
public var timeRemaining: TimeInterval?
|
|
/// Energiesparmodus. Erklärt, warum die Maschine langsamer wirkt — und
|
|
/// gehört deshalb sichtbar dorthin, wo man nach dem Grund sucht.
|
|
public var isLowPower = false
|
|
/// Temperatur der Zelle, falls der SMC sie hergibt.
|
|
public var celsius: Double?
|
|
}
|
|
|
|
/// Ein Programm mit seinem Speicherverbrauch.
|
|
public struct ProcessMemory: Equatable, Sendable, Identifiable {
|
|
public let pid: pid_t
|
|
public let name: String
|
|
public let bytes: UInt64
|
|
public var id: pid_t { pid }
|
|
}
|
|
|
|
public struct SensorReading: Equatable, Sendable, Identifiable {
|
|
public let key: String
|
|
public let name: String
|
|
public let celsius: Double
|
|
public var id: String { key }
|
|
}
|
|
|
|
public struct FanReading: Equatable, Sendable, Identifiable {
|
|
public let index: Int
|
|
public let name: String
|
|
public let rpm: Double
|
|
public let minimum: Double
|
|
public let maximum: Double
|
|
public var id: Int { index }
|
|
|
|
/// Anteil zwischen Minimum und Maximum, 0 bis 1.
|
|
public var fraction: Double {
|
|
guard maximum > minimum else { return 0 }
|
|
return min(max((rpm - minimum) / (maximum - minimum), 0), 1)
|
|
}
|
|
}
|
|
|
|
/// Liest die Hardwarewerte des Systems.
|
|
///
|
|
/// Bewusst kein Singleton mit Timer: das Abtasten steuert `MetricsModel`, weil
|
|
/// nur dort bekannt ist, wie viele Konsumenten es gerade gibt und wie schnell
|
|
/// sie es brauchen.
|
|
public final class SystemMetrics: @unchecked Sendable {
|
|
|
|
private let smc = SMCReader()
|
|
private var previousCPUTicks: [CPUTicks] = []
|
|
|
|
/// Welche Kern- und GPU-Fühler dieses Modell hat. `Tp*` sind die
|
|
/// CPU-Kerne, `Tg*` die GPU-Cluster — die Konvention gilt für die ganze
|
|
/// M-Serie.
|
|
///
|
|
/// Die Suche läuft im Hintergrund: sie geht alle 3486 Keys durch und
|
|
/// braucht dafür knapp eine Sekunde. Auf dem Hauptthread wäre das eine
|
|
/// spürbare Startverzögerung für etwas, das nach dem ersten Messwert
|
|
/// niemandem auffällt.
|
|
private var temperatureKeys: (cpu: [String], gpu: [String]) = ([], [])
|
|
private let keysLock = NSLock()
|
|
|
|
/// Zwischenspeicher für die Temperaturen.
|
|
///
|
|
/// Fünfzig Fühler zu lesen kostet gut 15 ms. Bei 1 Hz wären das anderthalb
|
|
/// Prozent CPU für Werte, die sich in einer Sekunde kaum bewegen — deshalb
|
|
/// höchstens alle zwei Sekunden, unabhängig von der Abtastrate ringsum.
|
|
private var cachedTemperatures: (cpu: Double?, gpu: Double?) = (nil, nil)
|
|
private var temperaturesReadAt = Date.distantPast
|
|
private static let temperatureInterval: TimeInterval = 2
|
|
|
|
/// Seit dem Start unveränderlich — einmal ermitteln reicht.
|
|
private static let pageSize: UInt64 = {
|
|
var size: UInt64 = 0
|
|
var length = MemoryLayout<UInt64>.size
|
|
guard sysctlbyname("hw.pagesize", &size, &length, nil, 0) == 0, size > 0 else {
|
|
return 16384 // Apple Silicon
|
|
}
|
|
return size
|
|
}()
|
|
|
|
public init() {
|
|
// Nicht im Initialisierer blockieren: der läuft beim App-Start.
|
|
// `DispatchQueue` statt `Task.detached`: die Suche ist blockierende
|
|
// IOKit-Arbeit ohne Suspendierungspunkt, sie gehört nicht in den
|
|
// Nebenläufigkeitspool von Swift — und `NSLock` ist aus einem
|
|
// asynchronen Kontext ohnehin nicht erlaubt.
|
|
DispatchQueue.global(qos: .utility).async { [weak self] in
|
|
guard let self, let smc else { return }
|
|
let cpu = smc.keys(matching: ["Tp"])
|
|
let gpu = smc.keys(matching: ["Tg"])
|
|
keysLock.lock()
|
|
temperatureKeys = (cpu, gpu)
|
|
keysLock.unlock()
|
|
}
|
|
}
|
|
|
|
public func sample() -> MetricsSnapshot {
|
|
var snapshot = MetricsSnapshot()
|
|
snapshot.cpu = readCPU()
|
|
snapshot.gpu = readGPU()
|
|
snapshot.memory = readMemory()
|
|
snapshot.battery = readBattery()
|
|
snapshot.sensors = readSensors()
|
|
snapshot.fans = readFans()
|
|
snapshot.topMemoryProcesses = readTopMemoryProcesses()
|
|
snapshot.power = readPower()
|
|
snapshot.cpu.watts = snapshot.power.cpu ?? snapshot.power.total
|
|
let temperatures = readTemperatures()
|
|
snapshot.cpu.temperature = temperatures.cpu
|
|
snapshot.gpuTemperature = temperatures.gpu
|
|
return snapshot
|
|
}
|
|
|
|
// MARK: - CPU
|
|
|
|
private func readCPU() -> CPUReading {
|
|
var count: natural_t = 0
|
|
var info: processor_info_array_t?
|
|
var infoCount: mach_msg_type_number_t = 0
|
|
|
|
guard host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO,
|
|
&count, &info, &infoCount) == KERN_SUCCESS,
|
|
let info else { return CPUReading() }
|
|
|
|
defer {
|
|
vm_deallocate(mach_task_self_, vm_address_t(bitPattern: info),
|
|
vm_size_t(infoCount) * vm_size_t(MemoryLayout<integer_t>.stride))
|
|
}
|
|
|
|
var current: [CPUTicks] = []
|
|
current.reserveCapacity(Int(count))
|
|
for core in 0..<Int(count) {
|
|
let base = core * Int(CPU_STATE_MAX)
|
|
current.append(CPUTicks(
|
|
user: UInt32(bitPattern: info[base + Int(CPU_STATE_USER)]),
|
|
system: UInt32(bitPattern: info[base + Int(CPU_STATE_SYSTEM)]),
|
|
idle: UInt32(bitPattern: info[base + Int(CPU_STATE_IDLE)]),
|
|
nice: UInt32(bitPattern: info[base + Int(CPU_STATE_NICE)])))
|
|
}
|
|
|
|
// Die erste Abtastung hat keinen Vorgänger und kann deshalb keine
|
|
// Auslastung ergeben. Null zu zeigen ist ehrlicher als der Anteil seit
|
|
// dem Systemstart, den man sonst bekäme — der liegt immer bei ein paar
|
|
// Prozent und sieht aus wie ein Messwert.
|
|
defer { previousCPUTicks = current }
|
|
guard previousCPUTicks.count == current.count else { return CPUReading() }
|
|
|
|
let perCore = zip(previousCPUTicks, current).map(CPUTicks.usage(from:to:))
|
|
return CPUReading(total: perCore.isEmpty ? 0 : perCore.reduce(0, +) / Double(perCore.count),
|
|
perCore: perCore,
|
|
watts: nil)
|
|
}
|
|
|
|
// MARK: - GPU
|
|
|
|
private func readGPU() -> Double? {
|
|
var iterator: io_iterator_t = 0
|
|
guard IOServiceGetMatchingServices(kIOMainPortDefault,
|
|
IOServiceMatching("IOAccelerator"),
|
|
&iterator) == kIOReturnSuccess else { return nil }
|
|
defer { IOObjectRelease(iterator) }
|
|
|
|
while case let entry = IOIteratorNext(iterator), entry != 0 {
|
|
defer { IOObjectRelease(entry) }
|
|
|
|
var unmanaged: Unmanaged<CFMutableDictionary>?
|
|
guard IORegistryEntryCreateCFProperties(entry, &unmanaged, kCFAllocatorDefault, 0)
|
|
== kIOReturnSuccess,
|
|
let properties = unmanaged?.takeRetainedValue() as? [String: Any],
|
|
let statistics = properties["PerformanceStatistics"] as? [String: Any]
|
|
else { continue }
|
|
|
|
// Der Schlüssel heißt je nach Gerät unterschiedlich; auf Apple
|
|
// Silicon ist es „Device Utilization %".
|
|
for name in ["Device Utilization %", "GPU Activity(%)", "Renderer Utilization %"] {
|
|
if let value = statistics[name] as? Int { return Double(value) / 100 }
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MARK: - Arbeitsspeicher
|
|
|
|
private func readMemory() -> MemoryReading {
|
|
var statistics = vm_statistics64_data_t()
|
|
var count = mach_msg_type_number_t(MemoryLayout<vm_statistics64_data_t>.size
|
|
/ MemoryLayout<integer_t>.size)
|
|
let result = withUnsafeMutablePointer(to: &statistics) {
|
|
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
|
|
host_statistics64(mach_host_self(), HOST_VM_INFO64, $0, &count)
|
|
}
|
|
}
|
|
guard result == KERN_SUCCESS else { return MemoryReading() }
|
|
|
|
// `vm_kernel_page_size` ist eine globale Variable und damit unter
|
|
// strenger Nebenläufigkeitsprüfung tabu. Der Wert steht seit dem Start
|
|
// fest, also einmal über sysctl holen.
|
|
let pageSize = Self.pageSize
|
|
let total = ProcessInfo.processInfo.physicalMemory
|
|
let compressed = UInt64(statistics.compressor_page_count) * pageSize
|
|
|
|
// „Benutzt" wie im Aktivitätsmonitor: aktiv, verdrahtet und komprimiert.
|
|
// Der Dateicache (`external_page_count`) zählt nicht mit — er wird bei
|
|
// Bedarf freigegeben, und ihn mitzuzählen ließe jeden Mac dauernd voll
|
|
// aussehen.
|
|
let used = UInt64(statistics.active_count) * pageSize
|
|
+ UInt64(statistics.wire_count) * pageSize
|
|
+ compressed
|
|
|
|
var swap = xsw_usage()
|
|
var swapSize = MemoryLayout<xsw_usage>.size
|
|
sysctlbyname("vm.swapusage", &swap, &swapSize, nil, 0)
|
|
|
|
let app = UInt64(statistics.active_count + statistics.inactive_count) * pageSize
|
|
let wired = UInt64(statistics.wire_count) * pageSize
|
|
let freeBytes = UInt64(statistics.free_count) * pageSize
|
|
|
|
let free = total > 0 ? 1 - Double(used) / Double(total) : 0
|
|
return MemoryReading(used: used, total: total, compressed: compressed,
|
|
swapUsed: swap.xsu_used, swapTotal: swap.xsu_total,
|
|
app: app, wired: wired, free: freeBytes,
|
|
pressure: MemoryPressure.classify(
|
|
free: free,
|
|
compressed: total > 0 ? Double(compressed) / Double(total) : 0,
|
|
swapUsed: swap.xsu_used))
|
|
}
|
|
|
|
// MARK: - Akku
|
|
|
|
private func readBattery() -> BatteryReading? {
|
|
guard let blob = IOPSCopyPowerSourcesInfo()?.takeRetainedValue(),
|
|
let sources = IOPSCopyPowerSourcesList(blob)?.takeRetainedValue() as? [CFTypeRef],
|
|
let source = sources.first,
|
|
let description = IOPSGetPowerSourceDescription(blob, source)?
|
|
.takeUnretainedValue() as? [String: Any] else { return nil }
|
|
|
|
var reading = BatteryReading()
|
|
if let current = description[kIOPSCurrentCapacityKey] as? Int,
|
|
let maximum = description[kIOPSMaxCapacityKey] as? Int, maximum > 0 {
|
|
reading.charge = Double(current) / Double(maximum)
|
|
}
|
|
reading.isCharging = description[kIOPSIsChargingKey] as? Bool ?? false
|
|
reading.isPluggedIn = (description[kIOPSPowerSourceStateKey] as? String)
|
|
== kIOPSACPowerValue
|
|
|
|
if let seconds = description[kIOPSTimeToEmptyKey] as? Int, seconds > 0 {
|
|
reading.timeRemaining = TimeInterval(seconds * 60)
|
|
}
|
|
if let seconds = description[kIOPSTimeToFullChargeKey] as? Int, seconds > 0 {
|
|
reading.timeRemaining = TimeInterval(seconds * 60)
|
|
}
|
|
|
|
// Zyklen und Gesundheit stehen nicht in der Power-Source-Beschreibung,
|
|
// sondern in der Batterie selbst.
|
|
let service = IOServiceGetMatchingService(kIOMainPortDefault,
|
|
IOServiceMatching("AppleSmartBattery"))
|
|
if service != 0 {
|
|
defer { IOObjectRelease(service) }
|
|
var unmanaged: Unmanaged<CFMutableDictionary>?
|
|
if IORegistryEntryCreateCFProperties(service, &unmanaged, kCFAllocatorDefault, 0)
|
|
== kIOReturnSuccess,
|
|
let properties = unmanaged?.takeRetainedValue() as? [String: Any] {
|
|
reading.cycleCount = properties["CycleCount"] as? Int
|
|
if let maximum = properties["MaxCapacity"] as? Int, maximum > 0 {
|
|
reading.health = Double(maximum) / 100
|
|
}
|
|
}
|
|
}
|
|
|
|
// Leistung aus dem SMC: positiv beim Laden, negativ beim Entladen.
|
|
reading.isLowPower = ProcessInfo.processInfo.isLowPowerModeEnabled
|
|
// TB0T ist die Akkutemperatur; nicht jede Maschine führt sie.
|
|
reading.celsius = smc?.float("TB0T").map { Double($0) }
|
|
if let watts = smc?.float("PPBR") {
|
|
reading.watts = reading.isCharging ? watts : -watts
|
|
}
|
|
return reading
|
|
}
|
|
|
|
/// Bei zwei Lüftern sind es links und rechts — so beschriftet Apple sie
|
|
/// selbst. Bei einer anderen Anzahl bleibt die Nummer, weil die Anordnung
|
|
/// dann nicht bekannt ist.
|
|
private static func fanName(index: Int, of count: Int) -> String {
|
|
guard count == 2 else { return "Lüfter \(index + 1)" }
|
|
return index == 0 ? "Lüfter links" : "Lüfter rechts"
|
|
}
|
|
|
|
// MARK: - Leistung
|
|
|
|
private func readPower() -> PowerReading {
|
|
guard let smc else { return PowerReading() }
|
|
return PowerReading(total: smc.float("PSTR"),
|
|
cpu: smc.float("PHPC"),
|
|
display: smc.float("PDBR"),
|
|
adapter: smc.float("PDTR"),
|
|
busVoltage: smc.float("VP0R"))
|
|
}
|
|
|
|
// MARK: - Speicher je Programm
|
|
|
|
/// Die größten Speicherverbraucher.
|
|
///
|
|
/// `phys_footprint` ist derselbe Wert, den der Aktivitätsmonitor unter
|
|
/// „Speicher" zeigt — nicht der virtuelle Adressraum, der bei modernen
|
|
/// Programmen sinnlos groß ist. Fremde Nutzer bleiben außen vor: dafür
|
|
/// bräuchte es root, und für einen Blick aufs eigene System reicht das hier.
|
|
private func readTopMemoryProcesses(limit: Int = 5) -> [ProcessMemory] {
|
|
var pids = [pid_t](repeating: 0, count: 4096)
|
|
let size = proc_listpids(UInt32(PROC_ALL_PIDS), 0, &pids,
|
|
Int32(pids.count * MemoryLayout<pid_t>.size))
|
|
guard size > 0 else { return [] }
|
|
|
|
let count = Int(size) / MemoryLayout<pid_t>.size
|
|
var results: [ProcessMemory] = []
|
|
|
|
for pid in pids.prefix(count) where pid > 0 {
|
|
var usage = rusage_info_v4()
|
|
let result = withUnsafeMutablePointer(to: &usage) {
|
|
$0.withMemoryRebound(to: rusage_info_t?.self, capacity: 1) {
|
|
proc_pid_rusage(pid, RUSAGE_INFO_V4, $0)
|
|
}
|
|
}
|
|
guard result == 0, usage.ri_phys_footprint > 64 * 1024 * 1024 else { continue }
|
|
|
|
var nameBuffer = [CChar](repeating: 0, count: Int(MAXPATHLEN))
|
|
proc_name(pid, &nameBuffer, UInt32(nameBuffer.count))
|
|
let name = String(cString: nameBuffer)
|
|
guard !name.isEmpty else { continue }
|
|
|
|
results.append(ProcessMemory(pid: pid, name: name, bytes: usage.ri_phys_footprint))
|
|
}
|
|
return Array(results.sorted { $0.bytes > $1.bytes }.prefix(limit))
|
|
}
|
|
|
|
// MARK: - Sensoren und Lüfter
|
|
|
|
/// Die Temperaturfühler, die auf `Mac17,9` tatsächlich existieren.
|
|
///
|
|
/// Bewusst eine kurze Liste statt aller 3486 SMC-Keys: die meisten sind
|
|
/// unbenannt, und eine Wand aus Kürzeln ist keine Information. Was fehlt,
|
|
/// fällt beim Lesen einfach weg.
|
|
private static let temperatureKeys: [(String, String)] = [
|
|
("TB0T", "Akku"),
|
|
("Ts0P", "Handballenauflage"),
|
|
("Ts1P", "Gehäuse hinten"),
|
|
("TW0P", "WLAN"),
|
|
("TH0x", "SSD"),
|
|
("TCMb", "Luftstrom"),
|
|
("TDeL", "Thunderbolt links"),
|
|
("TDeR", "Thunderbolt rechts"),
|
|
]
|
|
|
|
private func readTemperatures() -> (cpu: Double?, gpu: Double?) {
|
|
guard Date().timeIntervalSince(temperaturesReadAt) >= Self.temperatureInterval else {
|
|
return cachedTemperatures
|
|
}
|
|
keysLock.lock()
|
|
let keys = temperatureKeys
|
|
keysLock.unlock()
|
|
|
|
// Solange die Suche im Hintergrund läuft, bleiben die Werte leer — die
|
|
// Oberfläche zeigt dann „—" statt einer erfundenen Zahl.
|
|
guard !keys.cpu.isEmpty || !keys.gpu.isEmpty else { return (nil, nil) }
|
|
|
|
cachedTemperatures = (hottest(of: keys.cpu), hottest(of: keys.gpu))
|
|
temperaturesReadAt = Date()
|
|
return cachedTemperatures
|
|
}
|
|
|
|
/// Der höchste plausible Messwert einer Fühlergruppe.
|
|
///
|
|
/// Die Plausibilitätsgrenze ist nötig: einzelne Fühler liefern gelegentlich
|
|
/// 0 oder Werte jenseits von 200 °C, und ein einziger Ausreißer würde die
|
|
/// Anzeige übernehmen, weil hier das Maximum gebildet wird.
|
|
private func hottest(of keys: [String]) -> Double? {
|
|
guard let smc, !keys.isEmpty else { return nil }
|
|
return keys.compactMap { key -> Double? in
|
|
guard let value = smc.float(key), value > 5, value < 150 else { return nil }
|
|
return value
|
|
}.max()
|
|
}
|
|
|
|
private func readSensors() -> [SensorReading] {
|
|
guard let smc else { return [] }
|
|
let named = Self.temperatureKeys.compactMap { key, name -> SensorReading? in
|
|
guard let celsius = smc.float(key), celsius > 0, celsius < 150 else { return nil }
|
|
return SensorReading(key: key, name: name, celsius: celsius)
|
|
}
|
|
// CPU und GPU nach vorn: das sind die Werte, wegen derer man hinschaut.
|
|
let temperatures = readTemperatures()
|
|
var result: [SensorReading] = []
|
|
if let cpu = temperatures.cpu {
|
|
result.append(SensorReading(key: "cpu", name: "CPU-Kerne", celsius: cpu))
|
|
}
|
|
if let gpu = temperatures.gpu {
|
|
result.append(SensorReading(key: "gpu", name: "Grafik", celsius: gpu))
|
|
}
|
|
return result + named
|
|
}
|
|
|
|
private func readFans() -> [FanReading] {
|
|
guard let smc, let count = smc.integer("FNum"), count > 0 else { return [] }
|
|
return (0..<Int(count)).compactMap { index in
|
|
guard let rpm = smc.float("F\(index)Ac") else { return nil }
|
|
return FanReading(index: index,
|
|
name: Self.fanName(index: index, of: Int(count)),
|
|
rpm: rpm,
|
|
minimum: smc.float("F\(index)Mn") ?? 0,
|
|
maximum: smc.float("F\(index)Mx") ?? 0)
|
|
}
|
|
}
|
|
}
|