From a4166729ec6e6691e21f850f262ef03c88ab9009 Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Fri, 14 Jun 2019 15:06:45 +0200 Subject: [PATCH] initialized battery module --- Stats.xcodeproj/project.pbxproj | 16 +++ Stats/AppDelegate.swift | 2 +- Stats/Modules/Battery/Battery.swift | 145 +++++++++++++++++++ Stats/Modules/Battery/BatteryReader.swift | 164 ++++++++++++++++++++++ Stats/libs/Extensions.swift | 38 ++++- 5 files changed, 359 insertions(+), 6 deletions(-) create mode 100644 Stats/Modules/Battery/Battery.swift create mode 100644 Stats/Modules/Battery/BatteryReader.swift diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index f7169d29..77daa7aa 100755 --- a/Stats.xcodeproj/project.pbxproj +++ b/Stats.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 9A09C89E22B3A7C90018426F /* Battery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A09C89D22B3A7C90018426F /* Battery.swift */; }; + 9A09C8A022B3A7E20018426F /* BatteryReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A09C89F22B3A7E20018426F /* BatteryReader.swift */; }; 9A1410F9229E721100D29793 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1410F8229E721100D29793 /* AppDelegate.swift */; }; 9A141100229E721200D29793 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9A1410FE229E721200D29793 /* Main.storyboard */; }; 9A57A18522A1D26D0033E318 /* MenuBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A57A18422A1D26D0033E318 /* MenuBar.swift */; }; @@ -44,6 +46,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 9A09C89D22B3A7C90018426F /* Battery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Battery.swift; sourceTree = ""; }; + 9A09C89F22B3A7E20018426F /* BatteryReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryReader.swift; sourceTree = ""; }; 9A1410F5229E721100D29793 /* Stats.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Stats.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9A1410F8229E721100D29793 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 9A1410FF229E721200D29793 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -91,6 +95,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 9A09C89C22B3A7BB0018426F /* Battery */ = { + isa = PBXGroup; + children = ( + 9A09C89D22B3A7C90018426F /* Battery.swift */, + 9A09C89F22B3A7E20018426F /* BatteryReader.swift */, + ); + path = Battery; + sourceTree = ""; + }; 9A1410EC229E721100D29793 = { isa = PBXGroup; children = ( @@ -136,6 +149,7 @@ 9A5B1CBA229E7892008B9D3C /* Modules */ = { isa = PBXGroup; children = ( + 9A09C89C22B3A7BB0018426F /* Battery */, 9A7B8F5C22A2926500DEB352 /* CPU */, 9A7B8F6222A2C17000DEB352 /* Memory */, 9A7B8F6322A2C17500DEB352 /* Disk */, @@ -311,9 +325,11 @@ 9A7B8F6F22A2C57000DEB352 /* DiskReader.swift in Sources */, 9A7B8F6922A2C3A100DEB352 /* Memory.swift in Sources */, 9A7B8F5E22A2A57600DEB352 /* CPUReader.swift in Sources */, + 9A09C89E22B3A7C90018426F /* Battery.swift in Sources */, 9A7B8F6D22A2C3D600DEB352 /* MemoryReader.swift in Sources */, 9A57A18522A1D26D0033E318 /* MenuBar.swift in Sources */, 9A57A19D22A1E3270033E318 /* CPU.swift in Sources */, + 9A09C8A022B3A7E20018426F /* BatteryReader.swift in Sources */, 9A57A19B22A1E1C50033E318 /* Module.swift in Sources */, 9A5B1CBF229E78F0008B9D3C /* Observable.swift in Sources */, 9A7B8F6B22A2C3A700DEB352 /* Disk.swift in Sources */, diff --git a/Stats/AppDelegate.swift b/Stats/AppDelegate.swift index ab732e83..e7bce5d7 100755 --- a/Stats/AppDelegate.swift +++ b/Stats/AppDelegate.swift @@ -13,7 +13,7 @@ extension Notification.Name { static let killLauncher = Notification.Name("killLauncher") } -let modules: Observable<[Module]> = Observable([CPU(), Memory(), Disk()]) +let modules: Observable<[Module]> = Observable([CPU(), Memory(), Disk(), Battery()]) let colors: Observable = Observable(true) @NSApplicationMain diff --git a/Stats/Modules/Battery/Battery.swift b/Stats/Modules/Battery/Battery.swift new file mode 100644 index 00000000..19923b10 --- /dev/null +++ b/Stats/Modules/Battery/Battery.swift @@ -0,0 +1,145 @@ +// +// Battery.swift +// Stats +// +// Created by Serhiy Mytrovtsiy on 14/06/2019. +// Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved. +// + +import Cocoa + +class BatteryView: NSView { + var value: Float { + didSet { + self.needsDisplay = true + setNeedsDisplay(self.frame) + } + } + var charging: Bool { + didSet { + self.needsDisplay = true + setNeedsDisplay(self.frame) + } + } + + override init(frame: NSRect) { + self.value = 1.0 + self.charging = false + super.init(frame: frame) + self.wantsLayer = true + self.addSubview(NSView()) + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + + let x: CGFloat = 4.0 + let w: CGFloat = dirtyRect.size.width - (x * 2) + let h: CGFloat = 11.0 + let y: CGFloat = (dirtyRect.size.height - h) / 2 + let r: CGFloat = 1.0 + + let battery = NSBezierPath(roundedRect: NSRect(x: x-1, y: y, width: w-1, height: h), xRadius: r, yRadius: r) + + let bPX: CGFloat = x+w-2 + let bPY: CGFloat = (dirtyRect.size.height / 2) - 2 + let batteryPoint = NSBezierPath(roundedRect: NSRect(x: bPX, y: bPY, width: 2, height: 4), xRadius: r, yRadius: r) + if self.charging { + NSColor.systemGreen.set() + } else { + NSColor.labelColor.set() + } + batteryPoint.lineWidth = 1.1 + batteryPoint.stroke() + batteryPoint.fill() + + let maxWidth = w-4.25 + let inner = NSBezierPath(roundedRect: NSRect(x: x+0.75, y: y+1.5, width: maxWidth*CGFloat(self.value), height: h-3), xRadius: 0.5, yRadius: 0.5) + self.value.batteryColor().set() + inner.lineWidth = 0 + inner.stroke() + inner.close() + inner.fill() + + if self.charging { + NSColor.systemGreen.set() + } else { + NSColor.labelColor.set() + } + battery.lineWidth = 0.8 + battery.stroke() + } + + func changeValue(value: Float) { + if self.value != value { + self.value = value + } + } + + func setCharging(value: Bool) { + if self.charging != value { + self.charging = value + } + } +} + +class Battery: Module { + let name: String = "Battery" + var view: NSView = NSView() + let defaults = UserDefaults.standard + + var active: Observable + var reader: Reader = BatteryReader() + + init() { + self.active = Observable(defaults.object(forKey: name) != nil ? defaults.bool(forKey: name) : true) + self.view = BatteryView(frame: NSMakeRect(0, 0, MODULE_WIDTH, MODULE_HEIGHT)) + } + + func start() { + if !self.reader.usage.value.isNaN { + let value = self.reader.usage!.value + (self.view as! BatteryView).setCharging(value: value > 0) + (self.view as! BatteryView).changeValue(value: abs(value)) + } + + self.reader.start() + self.reader.usage.subscribe(observer: self) { (value, _) in + if !value.isNaN { + (self.view as! BatteryView).setCharging(value: value > 0) + (self.view as! BatteryView).changeValue(value: abs(value)) + } + } + } + + func menu() -> NSMenuItem { + let menu = NSMenuItem(title: name, action: #selector(toggle), keyEquivalent: "") + if defaults.object(forKey: name) != nil { + menu.state = defaults.bool(forKey: name) ? NSControl.StateValue.on : NSControl.StateValue.off + } else { + menu.state = NSControl.StateValue.on + } + menu.target = self + menu.isEnabled = true + return menu + } + + @objc func toggle(_ sender: NSMenuItem) { + let state = sender.state != NSControl.StateValue.on + + sender.state = sender.state == NSControl.StateValue.on ? NSControl.StateValue.off : NSControl.StateValue.on + self.defaults.set(state, forKey: name) + self.active << state + + if !state { + self.stop() + } else { + self.start() + } + } +} + diff --git a/Stats/Modules/Battery/BatteryReader.swift b/Stats/Modules/Battery/BatteryReader.swift new file mode 100644 index 00000000..a2a6eb94 --- /dev/null +++ b/Stats/Modules/Battery/BatteryReader.swift @@ -0,0 +1,164 @@ +// +// BatteryReader.swift +// Stats +// +// Created by Serhiy Mytrovtsiy on 14/06/2019. +// Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved. +// + +import Foundation + +class BatteryReader: Reader { + var usage: Observable! + var updateTimer: Timer! + + fileprivate static let IOSERVICE_BATTERY = "AppleSmartBattery" + fileprivate var service: io_service_t = 0 + fileprivate enum Key: String { + case ACPowered = "ExternalConnected" + case Amperage = "Amperage" + /// Current charge + case CurrentCapacity = "CurrentCapacity" + case CycleCount = "CycleCount" + /// Originally DesignCapacity == MaxCapacity + case DesignCapacity = "DesignCapacity" + case DesignCycleCount = "DesignCycleCount9C" + case FullyCharged = "FullyCharged" + case IsCharging = "IsCharging" + /// Current max charge (this degrades over time) + case MaxCapacity = "MaxCapacity" + case Temperature = "Temperature" + /// Time remaining to charge/discharge + case TimeRemaining = "TimeRemaining" + } + + init() { + self.usage = Observable(0) + read() + } + + func start() { + _ = self.open() + if updateTimer != nil { + return + } + updateTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(read), userInfo: nil, repeats: true) + } + + func stop() { + _ = self.close() + if updateTimer == nil { + return + } + updateTimer.invalidate() + updateTimer = nil + } + + @objc func read() { + var cap = charge() + let charging = isCharging() + + if !charging { + cap = 0 - cap + } + + self.usage << Float(cap) + } + + public func open() -> kern_return_t { + if (service != 0) { + #if DEBUG + print("WARNING - \(#file):\(#function) - connection already open") + #endif + return kIOReturnStillOpen + } + + service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceNameMatching("AppleSmartBattery")) + + if (service == 0) { + #if DEBUG + print("ERROR - \(#file):\(#function) - service not found") + #endif + return kIOReturnNotFound + } + + return kIOReturnSuccess + } + + public func close() -> kern_return_t { + let result = IOObjectRelease(service) + service = 0 + + #if DEBUG + if (result != kIOReturnSuccess) { + print("ERROR - \(#file):\(#function) - Failed to close") + } + #endif + + return result + } + + public func maxCapactiy() -> Int { + let prop = IORegistryEntryCreateCFProperty(service, Key.MaxCapacity.rawValue as CFString, kCFAllocatorDefault, 0) + + if prop != nil { + return prop!.takeUnretainedValue() as! Int + } + return 0 + } + + public func currentCapacity() -> Int { + let prop = IORegistryEntryCreateCFProperty(service, Key.CurrentCapacity.rawValue as CFString, kCFAllocatorDefault, 0) + + if prop != nil { + return prop!.takeUnretainedValue() as! Int + } + return 0 + } + + public func isACPowered() -> Bool { + let prop = IORegistryEntryCreateCFProperty(service, Key.ACPowered.rawValue as CFString, kCFAllocatorDefault, 0) + + if prop != nil { + return prop!.takeUnretainedValue() as! Bool + } + return false + } + + public func isCharging() -> Bool { + let prop = IORegistryEntryCreateCFProperty(service, Key.IsCharging.rawValue as CFString, kCFAllocatorDefault, 0) + + if prop != nil { + return prop!.takeUnretainedValue() as! Bool + } + return false + } + + public func isCharged() -> Bool { + let prop = IORegistryEntryCreateCFProperty(service, Key.FullyCharged.rawValue as CFString, kCFAllocatorDefault, 0) + + if prop != nil { + return prop!.takeUnretainedValue() as! Bool + } + return false + } + + public func charge() -> Double { + let ccap = Double(currentCapacity()) + let mcap = Double(maxCapactiy()) + + if ccap != 0 && mcap != 0 { + return ccap / mcap + } + return 0 + } + + public func timeRemaining() -> Int { + let prop = IORegistryEntryCreateCFProperty(service, Key.TimeRemaining.rawValue as CFString, kCFAllocatorDefault, 0) + + if prop != nil { + return prop!.takeUnretainedValue() as! Int + } + return 0 + } +} diff --git a/Stats/libs/Extensions.swift b/Stats/libs/Extensions.swift index 7a7f94b0..9a53f8b2 100755 --- a/Stats/libs/Extensions.swift +++ b/Stats/libs/Extensions.swift @@ -14,18 +14,46 @@ extension Float { return NSString(format: "%.\(decimalPlaces)f" as NSString, self) as String } - func usageColor() -> NSColor { + func usageColor(reversed: Bool = false) -> NSColor { if !colors.value { return NSColor.textColor } + if reversed { + switch self { + case 0.6...0.8: + return NSColor.systemOrange + case 0.8...1: + return NSColor.systemGreen + default: + return NSColor.systemRed + } + } else { + switch self { + case 0.6...0.8: + return NSColor.systemOrange + case 0.8...1: + return NSColor.systemRed + default: + return NSColor.systemGreen + } + } + } + + func batteryColor() -> NSColor { switch self { - case 0.6...0.8: + case 0.2...0.4: + if !colors.value { + return NSColor.controlTextColor + } return NSColor.systemOrange - case 0.8...1: - return NSColor.systemRed - default: + case 0.4...1: + if !colors.value { + return NSColor.controlTextColor + } return NSColor.systemGreen + default: + return NSColor.systemRed } } }