import Foundation import IOKit import IOKit.ps // The hardware reads. IOKit-only, no `powermetrics`, no root: // * CPU load — Mach `host_processor_info(PROCESSOR_CPU_LOAD_INFO)` deltas // * battery % — IOKit power sources (`IOPSCopyPowerSourcesInfo`) // * charging — IOKit power sources // * battery draw— IORegistry `AppleSmartBattery` (Amperage * Voltage) // * thermal — `ProcessInfo.thermalState` (aggregate enum, no root) // // GPU utilization and fan RPM are reachable in principle via `IOReport`/AppleSMC // but only with chip-generation-specific, undocumented channel spelunking. A // metric that cannot be read cleanly without root is *omitted from the schema* // rather than gated behind sudo. They are intentionally absent here. // // Every value produced is an aggregate scalar. There is no code path here that // reads user content — this file cannot, because it only ever touches hardware // counters and power registers. /// Cumulative CPU ticks summed across all cores. private struct CPUTicks { var used: UInt64 var total: UInt64 } /// Read cumulative CPU ticks via Mach `host_processor_info`. Non-privileged. private func readCPUTicks() -> CPUTicks? { var info: processor_info_array_t? var infoCount: mach_msg_type_number_t = 0 var cpuCount: natural_t = 0 let result = host_processor_info( mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &cpuCount, &info, &infoCount ) guard result == KERN_SUCCESS, let info else { return nil } defer { let addr = vm_address_t(UInt(bitPattern: UnsafeRawPointer(info))) vm_deallocate(mach_task_self_, addr, vm_size_t(infoCount) * vm_size_t(MemoryLayout.stride)) } let states = Int(CPU_STATE_MAX) var used: UInt64 = 0 var total: UInt64 = 0 for cpu in 0.. Double? { guard let cur = readCPUTicks() else { return nil } defer { prev = cur } guard let prev else { return nil } let dTotal = cur.total >= prev.total ? cur.total - prev.total : 0 let dUsed = cur.used >= prev.used ? cur.used - prev.used : 0 guard dTotal > 0 else { return nil } return min(1.0, Double(dUsed) / Double(dTotal)) } } /// A snapshot of the internal battery, if present. public struct BatteryInfo { public var pct: Double? public var charging: Bool? } /// Read battery percentage and charging state via IOKit power sources. No root. /// Returns empty fields on a machine with no internal battery (e.g. a desktop). public func readBattery() -> BatteryInfo { guard let blob = IOPSCopyPowerSourcesInfo()?.takeRetainedValue(), let list = IOPSCopyPowerSourcesList(blob)?.takeRetainedValue() as? [CFTypeRef] else { return BatteryInfo(pct: nil, charging: nil) } for source in list { guard let desc = IOPSGetPowerSourceDescription(blob, source)?.takeUnretainedValue() as? [String: Any] else { continue } if let type = desc[kIOPSTypeKey] as? String, type != kIOPSInternalBatteryType { continue } var pct: Double? if let cur = (desc[kIOPSCurrentCapacityKey] as? NSNumber)?.doubleValue, let max = (desc[kIOPSMaxCapacityKey] as? NSNumber)?.doubleValue, max > 0 { pct = cur / max * 100.0 } let charging = (desc[kIOPSIsChargingKey] as? NSNumber)?.boolValue return BatteryInfo(pct: pct, charging: charging) } return BatteryInfo(pct: nil, charging: nil) } /// Instantaneous battery draw in watts from the IORegistry `AppleSmartBattery` /// node (Amperage in mA, Voltage in mV). No root. `nil` on a machine with no /// smart battery. public func readBatteryDrawW() -> Double? { let service = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("AppleSmartBattery")) guard service != 0 else { return nil } defer { IOObjectRelease(service) } func number(_ key: String) -> Double? { guard let cf = IORegistryEntryCreateCFProperty(service, key as CFString, kCFAllocatorDefault, 0)? .takeRetainedValue() as? NSNumber else { return nil } return cf.doubleValue } guard let amperage = number("Amperage"), let voltage = number("Voltage") else { return nil } // Amperage is signed (negative = discharging); draw magnitude is what we map. return abs(amperage / 1000.0 * voltage / 1000.0) } /// Aggregate thermal state as `0..3` (nominal/fair/serious/critical). Sourced /// from `ProcessInfo`, not `powermetrics`; no root, no per-sensor detail. public func readThermalState() -> Double { switch ProcessInfo.processInfo.thermalState { case .nominal: return 0 case .fair: return 1 case .serious: return 2 case .critical: return 3 @unknown default: return 0 } } /// Collect one tick of hardware signals. Signals whose sensor is absent (no /// battery, priming CPU sample) are simply omitted — never faked. public func collectHardwareSignals(cpu: CPUSampler, now: UInt64) -> [Signal] { var out: [Signal] = [] if let load = cpu.sample() { out.append(Signal(ts: now, source: .hardware, name: .cpuLoad, value: load)) } let battery = readBattery() if let pct = battery.pct { out.append(Signal(ts: now, source: .macos, name: .batteryPct, value: pct)) } if let charging = battery.charging { out.append(Signal(ts: now, source: .macos, name: .charging, value: charging ? 1.0 : 0.0)) } if let watts = readBatteryDrawW() { out.append(Signal(ts: now, source: .hardware, name: .batteryDrawW, value: watts)) } out.append(Signal(ts: now, source: .macos, name: .thermalState, value: readThermalState())) return out }