mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-14 00:04:15 +09:00
237 lines
8.7 KiB
Swift
237 lines
8.7 KiB
Swift
//
|
|
// readers.swift
|
|
// Battery
|
|
//
|
|
// Created by Serhiy Mytrovtsiy on 06/06/2020.
|
|
// Using Swift 5.0.
|
|
// Running on macOS 10.15.
|
|
//
|
|
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
|
//
|
|
|
|
import Cocoa
|
|
import Kit
|
|
|
|
internal class UsageReader: Reader<Battery_Usage> {
|
|
private var service: io_connect_t = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("AppleSmartBattery"))
|
|
|
|
private var source: CFRunLoopSource?
|
|
private var loop: CFRunLoop?
|
|
|
|
private var usage: Battery_Usage = Battery_Usage()
|
|
|
|
public override func start() {
|
|
self.active = true
|
|
let context = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
|
|
|
|
self.source = IOPSNotificationCreateRunLoopSource({ (context) in
|
|
guard let ctx = context else {
|
|
return
|
|
}
|
|
|
|
let watcher = Unmanaged<UsageReader>.fromOpaque(ctx).takeUnretainedValue()
|
|
if watcher.active {
|
|
watcher.read()
|
|
}
|
|
}, context).takeRetainedValue()
|
|
|
|
self.loop = RunLoop.current.getCFRunLoop()
|
|
CFRunLoopAddSource(self.loop, source, .defaultMode)
|
|
|
|
self.read()
|
|
|
|
if #available(macOS 12.0, *) {
|
|
NotificationCenter.default.addObserver(self, selector: #selector(self.lowModeChanged), name: Notification.Name.NSProcessInfoPowerStateDidChange, object: nil)
|
|
}
|
|
}
|
|
|
|
public override func stop() {
|
|
guard let runLoop = loop, let source = source else {
|
|
return
|
|
}
|
|
|
|
self.active = false
|
|
CFRunLoopRemoveSource(runLoop, source, .defaultMode)
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
|
|
public override func read() {
|
|
let psInfo = IOPSCopyPowerSourcesInfo().takeRetainedValue()
|
|
let psList = IOPSCopyPowerSourcesList(psInfo).takeRetainedValue() as [CFTypeRef]
|
|
|
|
if psList.isEmpty {
|
|
return
|
|
}
|
|
|
|
for ps in psList {
|
|
if let list = IOPSGetPowerSourceDescription(psInfo, ps).takeUnretainedValue() as? [String: Any] {
|
|
self.usage.powerSource = list[kIOPSPowerSourceStateKey] as? String ?? "AC Power"
|
|
self.usage.isBatteryPowered = self.usage.powerSource == "Battery Power"
|
|
self.usage.isCharged = list[kIOPSIsChargedKey] as? Bool ?? false
|
|
self.usage.isCharging = self.getBoolValue("IsCharging" as CFString) ?? false
|
|
self.usage.optimizedChargingEngaged = list["Optimized Battery Charging Engaged"] as? Int == 1
|
|
if #available(macOS 12.0, *) {
|
|
self.usage.isLowPowerMode = ProcessInfo.processInfo.isLowPowerModeEnabled
|
|
}
|
|
self.usage.level = Double(list[kIOPSCurrentCapacityKey] as? Int ?? 0) / 100
|
|
|
|
if let time = list[kIOPSTimeToEmptyKey] as? Int {
|
|
self.usage.timeToEmpty = Int(time)
|
|
}
|
|
if let time = list[kIOPSTimeToFullChargeKey] as? Int {
|
|
self.usage.timeToCharge = Int(time)
|
|
}
|
|
|
|
if self.usage.powerSource == "AC Power" {
|
|
self.usage.timeOnACPower = Date()
|
|
}
|
|
|
|
self.usage.cycles = self.getIntValue("CycleCount" as CFString) ?? 0
|
|
|
|
self.usage.currentCapacity = self.getIntValue("AppleRawCurrentCapacity" as CFString) ?? 0
|
|
self.usage.designedCapacity = self.getIntValue("DesignCapacity" as CFString) ?? 1
|
|
self.usage.maxCapacity = self.getIntValue((isARM ? "AppleRawMaxCapacity" : "MaxCapacity") as CFString) ?? 1
|
|
if !isARM {
|
|
self.usage.state = list[kIOPSBatteryHealthKey] as? String
|
|
}
|
|
self.usage.health = Int((Double(100 * self.usage.maxCapacity) / Double(self.usage.designedCapacity)).rounded(.toNearestOrEven))
|
|
|
|
self.usage.amperage = self.getIntValue("Amperage" as CFString) ?? 0
|
|
self.usage.voltage = self.getVoltage() ?? 0
|
|
self.usage.temperature = self.getTemperature() ?? 0
|
|
|
|
var ACwatts: Int = 0
|
|
if let ACDetails = IOPSCopyExternalPowerAdapterDetails() {
|
|
if let ACList = ACDetails.takeRetainedValue() as? [String: Any] {
|
|
guard let watts = ACList[kIOPSPowerAdapterWattsKey] else {
|
|
return
|
|
}
|
|
ACwatts = Int(watts as! Int)
|
|
}
|
|
}
|
|
self.usage.ACwatts = ACwatts
|
|
|
|
self.callback(self.usage)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func getBoolValue(_ forIdentifier: CFString) -> Bool? {
|
|
if let value = IORegistryEntryCreateCFProperty(self.service, forIdentifier, kCFAllocatorDefault, 0) {
|
|
return value.takeRetainedValue() as? Bool
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func getIntValue(_ identifier: CFString) -> Int? {
|
|
if let value = IORegistryEntryCreateCFProperty(self.service, identifier, kCFAllocatorDefault, 0) {
|
|
return value.takeRetainedValue() as? Int
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func getDoubleValue(_ identifier: CFString) -> Double? {
|
|
if let value = IORegistryEntryCreateCFProperty(self.service, identifier, kCFAllocatorDefault, 0) {
|
|
return value.takeRetainedValue() as? Double
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func getVoltage() -> Double? {
|
|
if let value = self.getDoubleValue("Voltage" as CFString) {
|
|
return value / 1000.0
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func getTemperature() -> Double? {
|
|
if let value = IORegistryEntryCreateCFProperty(self.service, "Temperature" as CFString, kCFAllocatorDefault, 0) {
|
|
return value.takeRetainedValue() as! Double / 100.0
|
|
}
|
|
return nil
|
|
}
|
|
|
|
@objc private func lowModeChanged() {
|
|
if #available(macOS 12.0, *) {
|
|
self.usage.isLowPowerMode = ProcessInfo.processInfo.isLowPowerModeEnabled
|
|
self.callback(self.usage)
|
|
}
|
|
}
|
|
}
|
|
|
|
public class ProcessReader: Reader<[TopProcess]> {
|
|
private var numberOfProcesses: Int {
|
|
get {
|
|
return Store.shared.int(key: "Battery_processes", defaultValue: 8)
|
|
}
|
|
}
|
|
|
|
public override func setup() {
|
|
self.popup = true
|
|
}
|
|
|
|
public override func read() {
|
|
if self.numberOfProcesses == 0 {
|
|
return
|
|
}
|
|
|
|
let task = Process()
|
|
task.launchPath = "/bin/ps"
|
|
task.launchPath = "/usr/bin/top"
|
|
task.arguments = ["-o", "power", "-l", "2", "-n", "\(self.numberOfProcesses)", "-stats", "pid,command,power"]
|
|
|
|
let outputPipe = Pipe()
|
|
defer {
|
|
outputPipe.fileHandleForReading.closeFile()
|
|
}
|
|
task.standardOutput = outputPipe
|
|
|
|
do {
|
|
try task.run()
|
|
} catch let err {
|
|
error("error read ps: \(err.localizedDescription)", log: self.log)
|
|
return
|
|
}
|
|
|
|
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
|
|
if outputData.isEmpty {
|
|
return
|
|
}
|
|
|
|
let output = String(decoding: outputData.advanced(by: outputData.count/2), as: UTF8.self)
|
|
if output.isEmpty {
|
|
return
|
|
}
|
|
|
|
var processes: [TopProcess] = []
|
|
output.enumerateLines { (line, _) -> Void in
|
|
if line.matches("^\\d+ *[^(\\d)]*\\d+\\.*\\d* *$") {
|
|
var str = line.trimmingCharacters(in: .whitespaces)
|
|
|
|
let pidString = str.findAndCrop(pattern: "^\\d+")
|
|
let usageString = str.findAndCrop(pattern: " +[0-9]+.*[0-9]*$")
|
|
let command = str.trimmingCharacters(in: .whitespaces)
|
|
|
|
let pid = Int(pidString) ?? 0
|
|
guard let usage = Double(usageString.filter("01234567890.".contains)) else {
|
|
return
|
|
}
|
|
|
|
var name: String? = nil
|
|
var icon: NSImage? = nil
|
|
if let app = NSRunningApplication(processIdentifier: pid_t(pid) ) {
|
|
name = app.localizedName ?? nil
|
|
icon = app.icon
|
|
}
|
|
|
|
processes.append(TopProcess(pid: pid, command: command, name: name, usage: usage, icon: icon))
|
|
}
|
|
}
|
|
|
|
self.callback(processes.suffix(self.numberOfProcesses).sorted(by: { $0.usage > $1.usage }))
|
|
}
|
|
}
|