On solving a problem with a new language
I was running into a weird issue with my new Macbook Pro and its Magsafe port. Namely, with my 130 W GaN charger, and my stock Apple Magsafe cable, I was only seeing 60W out of the charger. At one point, with the system going full throttle on some projects, I was depleting the battery while on mains. I was curious how much power was actually being supplied to my system, and that information is buried in the System Information.app - present, just tucked away. I figured why not write a quick program to do this. Surely Apple has some interface to find] this programmatically. It turns out they do, but it was a bit of a mess getting it working.
I ended up doing all this XCode just given that it would have out-of-the-box library handling and whatnot. At first, I tried C++ / Objective-C, but couldn't get it working, so I decided why not give Swift a try. I once gave it a very brief go a few years ago, but haven't touched it since then and remember approximately none of it. My goal in doing this was to have something working correctly as quickly as possible. That is, my goal was to answer a question, not learn a new programming language. Swift devs may wish to avert their eyes.
I started with the base function to get power adapter information: IOPSCopyPowerSourcesInfo. Noticing the "Unmanaged" type that this returned, I did some very light skimming of docs and that turned into the line:
var powerSourceRef = IOPSCopyPowerSourcesInfo().takeRetainedValue()
So that's something. Now, need to turn this into a list of power sources. Right now, powerSourceRef is a CFTypeRef, which I guess is an opaque pointer blob? Anyways, the documentation for IOPSCopyPowerSourcesInfo says
Clients should not directly access data in the returned CFTypeRef - they should use the accessor functions IOPSCopyPowerSourcesList and IOPSGetPowerSourceDescription, instead.
All right, so let's look up those functions. IOPSGetPowerSourceDescription looks like it works on elements retrieved from IOPSCopyPowerSourcesList, so maybe the next step is to use that:
var powerSourceList = IOPSCopyPowerSourcesList(powerSourceRef)
.takeRetainedValue() as Array
var onMains = false
We're dealing with a list of power sources; I'm not exactly sure what this looks like, but we might get a power source for the battery and one for the adapter, or we might get only one, or maybe there's more and I don't really know. Some print debugging shows that entries appear to have keys for "Is Charging" and "Is Charged" - we'll need to check both to see if we're on mains [1].
Also, the as Array bit took some figuring out. At first I was dealing with underlying Foundation types (if I remember right, NSArray) and it ended up being messy. I searched for how to work with NSArray in Swift, and got back the "just cast as an array." Cool, cool.
The taking retained value, by the way, I figured out from Stack Overflow. According to this post, the rule of thumb is to use a retained value for functions with Copy in the name. So that's what I used here. I'm not sure the distinction is important for a CLI program that starts, prints a value, and exits. Anyways, time to look at our power sources.
for supply in powerSourceList {
let description = IOPSGetPowerSourceDescription(powerSourceRef,supply)
.takeUnretainedValue() as? [NSString: Any]
if (description?["Is Charging"] as? Int == 1) {
onMains = true
} else if (description?["Is Charged"] as? Int == 1) {
onMains = true
}
}
if (!onMains) {
print("System is not on mains power.")
exit(1)
}
I'll be honest, I had to finangle the casting and unwrapping here to get it working. I basically brute-forced a working solution. It works.
But none of this gives me the charger's wattage. Okay, more digging through docs and I find IOPSCopyExternalPowerAdapterDetails, which leads to
var acAdapter = IOPSCopyExternalPowerAdapterDetails()
.takeRetainedValue() as? [NSString: Any]
let wattage = acAdapter?["Watts"] ?? 0
print(wattage, "W")
I'm using the fact that we know we're on mains to assume there's a valid external power adapter. And to be honest, this is going to work pretty much every time I want to use it, so there's not much point in tweaking it to check for a nil value.
But... Curiousity is a hell of a drug. What if I skip the power supply junk above and code golf this thing a little? That leads to
var acAdapterOption = IOPSCopyExternalPowerAdapterDetails()
if (acAdapterOption != nil) {
let acAdapter = acAdapterOption?.takeRetainedValue() as? [NSString: Any]
let wattage = acAdapter?["Watts"] ?? 0
print(wattage, "W")
} else {
print("System is not on mains power.")
exit(1)
}
Okay, I'm not happy with this, but it undeniably works the way I think it should. There's almost certainly a better way to do this [2] but I'm calling it done for now. The whole program becomes
// wattage.swift
import Foundation
import IOKit.ps
var acAdapterOption = IOPSCopyExternalPowerAdapterDetails()
if (acAdapterOption != nil) {
let acAdapter = acAdapterOption?.takeRetainedValue() as? [NSString: Any]
let wattage = acAdapter?["Watts"] ?? 0
print(wattage, "W")
} else {
print("System is not on mains power.")
exit(1)
}
Time to do other things.
| [1] | There could absolutely be something else I'm missing here. |
| [2] | I have to believe devs working in Swift on a daily basis wouldn't want to put up with this. |