Files
MacMonitor/Sources/App/SystemTelemetryStore.swift
gronod f2dc6bc7fb
MacMonitor CI/CD Pipeline / Build & Test (Intel x86_64) (push) Canceled after 0s
feat(ui): bind all primary telemetry providers to SystemTelemetryStore and ContentView dashboard
2026-09-08 12:27:02 +01:00

313 lines
13 KiB
Swift

import Foundation
import SwiftUI
import Observation
// MARK: - Typed Telemetry Models
public struct CPULoadMetrics: Sendable {
public var systemLoad: Double = 0
public var userLoad: Double = 0
public var idleLoad: Double = 100
public var totalLoad: Double = 0
public var perCoreLoad: [Double] = []
public var cpuFrequencyMHz: Double = 0
public var isThrottled: Bool = false
}
public struct MemoryMetrics: Sendable {
public var totalBytes: UInt64 = 0
public var usedBytes: UInt64 = 0
public var freeBytes: UInt64 = 0
public var activeBytes: UInt64 = 0
public var inactiveBytes: UInt64 = 0
public var wiredBytes: UInt64 = 0
public var compressedBytes: UInt64 = 0
public var swapTotalBytes: UInt64 = 0
public var swapUsedBytes: UInt64 = 0
public var utilizationPercentage: Double = 0
public var pressureLevel: String = "Normal"
}
public struct StorageVolumeItem: Identifiable, Sendable {
public var id: String { mountPoint }
public let mountPoint: String
public let volumeName: String
public let fileSystem: String
public let totalBytes: UInt64
public let freeBytes: UInt64
public let usedBytes: UInt64
public let usedPercentage: Double
public let isReadOnly: Bool
}
public struct CPUThermalMetrics: Sendable {
public var packageTemperature: Double = 0
public var averageCoreTemperature: Double = 0
public var peakCoreTemperature: Double = 0
public var coreTemperatures: [(index: Int, key: String, temperature: Double)] = []
public var thermalPressureState: String = "Nominal"
public var isThrottling: Bool = false
}
public struct FanTelemetryItem: Identifiable, Sendable {
public var id: Int { index }
public let index: Int
public let name: String
public let currentRPM: Double
public let minRPM: Double
public let maxRPM: Double
public let targetRPM: Double
public let utilization: Double
}
public struct ComponentThermalItem: Identifiable, Sendable {
public var id: String { key }
public let key: String
public let name: String
public let category: String
public let temperature: Double
}
public struct PowerMetrics: Sendable {
public var systemTotalWatts: Double = 0
public var cpuWatts: Double = 0
public var gpuWatts: Double = 0
public var memoryWatts: Double = 0
public var cpuVoltage: Double = 0
public var cpuCurrent: Double = 0
public var powerSource: String = "AC Power"
public var isCharging: Bool = false
public var batteryLevel: Double = 100.0
public var sensorReadings: [(key: String, name: String, value: Double, unit: String)] = []
}
// MARK: - SystemTelemetryStore
@Observable
@MainActor
public final class SystemTelemetryStore {
public static let shared = SystemTelemetryStore()
public private(set) var isRunning: Bool = false
public private(set) var lastUpdateTimestamp: Date = .now
public private(set) var sampleInterval: TimeInterval = 1.0
/// Raw snapshots organized by provider identifier
public private(set) var latestSnapshot: [String: [String: Any]] = [:]
/// Structured Telemetry Metrics
public private(set) var cpuLoad = CPULoadMetrics()
public private(set) var memory = MemoryMetrics()
public private(set) var storageVolumes: [StorageVolumeItem] = []
public private(set) var cpuThermal = CPUThermalMetrics()
public private(set) var fans: [FanTelemetryItem] = []
public private(set) var componentTemps: [ComponentThermalItem] = []
public private(set) var power = PowerMetrics()
/// System identification
public let hostModel: String
public let osVersion: String
public let kernelVersion: String
public let physicalCpuCount: Int
public let logicalCpuCount: Int
private let coordinator = MMTelemetryCoordinator.shared
private init() {
self.hostModel = SystemTelemetryStore.readSysctlString("hw.model") ?? "Intel Mac"
self.osVersion = ProcessInfo.processInfo.operatingSystemVersionString
self.kernelVersion = SystemTelemetryStore.readSysctlString("kern.osrelease") ?? "Unknown Kernel"
self.physicalCpuCount = SystemTelemetryStore.readSysctlInt("hw.physicalcpu") ?? 1
self.logicalCpuCount = SystemTelemetryStore.readSysctlInt("hw.logicalcpu") ?? 1
registerDefaultProviders()
setupCoordinator()
}
private func registerDefaultProviders() {
// Register all Primary System & Thermal Telemetry providers (M2)
coordinator.register(MMCPULoadProvider())
coordinator.register(MMMemoryTelemetryProvider())
coordinator.register(MMStorageTelemetryProvider())
coordinator.register(MMCPUThermalProvider())
coordinator.register(MMFanTelemetryProvider())
coordinator.register(MMComponentThermalProvider())
coordinator.register(MMPowerTelemetryProvider())
}
private func setupCoordinator() {
coordinator.sampleInterval = self.sampleInterval
coordinator.snapshotHandler = { [weak self] snapshot in
guard let self else { return }
self.latestSnapshot = snapshot
self.lastUpdateTimestamp = Date()
self.decodeSnapshot(snapshot)
}
}
private func decodeSnapshot(_ snapshot: [String: [String: Any]]) {
// 1. CPU Load
if let cpuDict = snapshot["com.i3omb.macmonitor.telemetry.cpu"] {
var m = CPULoadMetrics()
m.systemLoad = (cpuDict["systemLoad"] as? NSNumber)?.doubleValue ?? 0
m.userLoad = (cpuDict["userLoad"] as? NSNumber)?.doubleValue ?? 0
m.idleLoad = (cpuDict["idleLoad"] as? NSNumber)?.doubleValue ?? 100
m.totalLoad = (cpuDict["totalLoad"] as? NSNumber)?.doubleValue ?? 0
if let perCore = cpuDict["perCoreLoad"] as? [NSNumber] {
m.perCoreLoad = perCore.map { $0.doubleValue }
}
m.cpuFrequencyMHz = (cpuDict["cpuFrequencyMHz"] as? NSNumber)?.doubleValue ?? 0
m.isThrottled = (cpuDict["isThrottled"] as? NSNumber)?.boolValue ?? false
self.cpuLoad = m
}
// 2. Memory
if let memDict = snapshot["com.i3omb.macmonitor.telemetry.memory"] {
var m = MemoryMetrics()
m.totalBytes = (memDict["totalBytes"] as? NSNumber)?.uint64Value ?? 0
m.usedBytes = (memDict["usedBytes"] as? NSNumber)?.uint64Value ?? 0
m.freeBytes = (memDict["freeBytes"] as? NSNumber)?.uint64Value ?? 0
m.activeBytes = (memDict["activeBytes"] as? NSNumber)?.uint64Value ?? 0
m.inactiveBytes = (memDict["inactiveBytes"] as? NSNumber)?.uint64Value ?? 0
m.wiredBytes = (memDict["wiredBytes"] as? NSNumber)?.uint64Value ?? 0
m.compressedBytes = (memDict["compressedBytes"] as? NSNumber)?.uint64Value ?? 0
m.swapTotalBytes = (memDict["swapTotalBytes"] as? NSNumber)?.uint64Value ?? 0
m.swapUsedBytes = (memDict["swapUsedBytes"] as? NSNumber)?.uint64Value ?? 0
m.utilizationPercentage = (memDict["utilizationPercentage"] as? NSNumber)?.doubleValue ?? 0
m.pressureLevel = memDict["pressureLevel"] as? String ?? "Normal"
self.memory = m
}
// 3. Storage
if let stgDict = snapshot["com.i3omb.macmonitor.telemetry.storage"],
let volArray = stgDict["volumes"] as? [[String: Any]] {
self.storageVolumes = volArray.compactMap { dict in
guard let mount = dict["mountPoint"] as? String else { return nil }
return StorageVolumeItem(
mountPoint: mount,
volumeName: dict["volumeName"] as? String ?? mount,
fileSystem: dict["fileSystem"] as? String ?? "APFS",
totalBytes: (dict["totalBytes"] as? NSNumber)?.uint64Value ?? 0,
freeBytes: (dict["freeBytes"] as? NSNumber)?.uint64Value ?? 0,
usedBytes: (dict["usedBytes"] as? NSNumber)?.uint64Value ?? 0,
usedPercentage: (dict["usedPercentage"] as? NSNumber)?.doubleValue ?? 0,
isReadOnly: (dict["isReadOnly"] as? NSNumber)?.boolValue ?? false
)
}
}
// 4. CPU Thermal
if let thDict = snapshot["com.i3omb.macmonitor.telemetry.thermal.cpu"] {
var m = CPUThermalMetrics()
m.packageTemperature = (thDict["packageTemperature"] as? NSNumber)?.doubleValue ?? 0
m.averageCoreTemperature = (thDict["averageCoreTemperature"] as? NSNumber)?.doubleValue ?? 0
m.peakCoreTemperature = (thDict["peakCoreTemperature"] as? NSNumber)?.doubleValue ?? 0
m.thermalPressureState = thDict["thermalPressureState"] as? String ?? "Nominal"
m.isThrottling = (thDict["isThrottling"] as? NSNumber)?.boolValue ?? false
if let coresList = thDict["coreTemperatures"] as? [[String: Any]] {
m.coreTemperatures = coresList.compactMap { d in
let idx = (d["coreIndex"] as? NSNumber)?.intValue ?? 0
let key = d["key"] as? String ?? "TC\(idx)C"
let temp = (d["temperature"] as? NSNumber)?.doubleValue ?? 0
return (index: idx, key: key, temperature: temp)
}
}
self.cpuThermal = m
}
// 5. Fans
if let fanDict = snapshot["com.i3omb.macmonitor.telemetry.fan"],
let fanArray = fanDict["fans"] as? [[String: Any]] {
self.fans = fanArray.compactMap { d in
let idx = (d["index"] as? NSNumber)?.intValue ?? 0
return FanTelemetryItem(
index: idx,
name: d["name"] as? String ?? "Fan \(idx + 1)",
currentRPM: (d["currentRPM"] as? NSNumber)?.doubleValue ?? 0,
minRPM: (d["minRPM"] as? NSNumber)?.doubleValue ?? 0,
maxRPM: (d["maxRPM"] as? NSNumber)?.doubleValue ?? 0,
targetRPM: (d["targetRPM"] as? NSNumber)?.doubleValue ?? 0,
utilization: (d["utilization"] as? NSNumber)?.doubleValue ?? 0
)
}
}
// 6. Component Thermals
if let compDict = snapshot["com.i3omb.macmonitor.telemetry.thermal.components"],
let compArray = compDict["components"] as? [[String: Any]] {
self.componentTemps = compArray.compactMap { d in
guard let key = d["key"] as? String else { return nil }
return ComponentThermalItem(
key: key,
name: d["name"] as? String ?? key,
category: d["category"] as? String ?? "Other",
temperature: (d["temperature"] as? NSNumber)?.doubleValue ?? 0
)
}
}
// 7. Power
if let pwrDict = snapshot["com.i3omb.macmonitor.telemetry.power"] {
var m = PowerMetrics()
m.systemTotalWatts = (pwrDict["systemTotalWatts"] as? NSNumber)?.doubleValue ?? 0
m.cpuWatts = (pwrDict["cpuWatts"] as? NSNumber)?.doubleValue ?? 0
m.gpuWatts = (pwrDict["gpuWatts"] as? NSNumber)?.doubleValue ?? 0
m.memoryWatts = (pwrDict["memoryWatts"] as? NSNumber)?.doubleValue ?? 0
m.cpuVoltage = (pwrDict["cpuVoltage"] as? NSNumber)?.doubleValue ?? 0
m.cpuCurrent = (pwrDict["cpuCurrent"] as? NSNumber)?.doubleValue ?? 0
m.powerSource = pwrDict["powerSource"] as? String ?? "AC Power"
m.isCharging = (pwrDict["isCharging"] as? NSNumber)?.boolValue ?? false
m.batteryLevel = (pwrDict["batteryLevel"] as? NSNumber)?.doubleValue ?? 100.0
if let sensorList = pwrDict["sensors"] as? [[String: Any]] {
m.sensorReadings = sensorList.compactMap { d in
guard let k = d["key"] as? String else { return nil }
let n = d["name"] as? String ?? k
let v = (d["value"] as? NSNumber)?.doubleValue ?? 0
let u = d["unit"] as? String ?? ""
return (key: k, name: n, value: v, unit: u)
}
}
self.power = m
}
}
public func start() {
coordinator.start()
isRunning = coordinator.isRunning
}
public func stop() {
coordinator.stop()
isRunning = coordinator.isRunning
}
public func setSampleInterval(_ interval: TimeInterval) {
self.sampleInterval = interval
coordinator.sampleInterval = interval
}
public func triggerManualSample() {
coordinator.sampleImmediately()
}
public var registeredProviderCount: Int {
coordinator.registeredProviders().count
}
private static func readSysctlString(_ name: String) -> String? {
var size: size_t = 0
sysctlbyname(name, nil, &size, nil, 0)
guard size > 0 else { return nil }
var buffer = [CChar](repeating: 0, count: size)
sysctlbyname(name, &buffer, &size, nil, 0)
return String(cString: buffer)
}
private static func readSysctlInt(_ name: String) -> Int? {
var value: Int32 = 0
var size = MemoryLayout<Int32>.size
let result = sysctlbyname(name, &value, &size, nil, 0)
return result == 0 ? Int(value) : nil
}
}