Last active
January 31, 2024 02:41
-
-
Save dreness/c83813479c458f4a274d755b967fc973 to your computer and use it in GitHub Desktop.
Display rate of charge / discharge of an Apple laptop battery
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!python | |
# To use this, you need Python 3 and also the pyobjc module. | |
# pip install pyobjc | |
import objc | |
from Foundation import NSBundle, NSString | |
import datetime | |
import time | |
import sys | |
""" | |
ioreg -l -n AppleSmartBattery -r | |
To learn about battery time remaining calculations: | |
https://opensource.apple.com/source/PowerManagement/PowerManagement-637.20.2/pmconfigd/BatteryTimeRemaining.c.auto.html | |
To learn about pmset's rawlog: | |
https://opensource.apple.com/source/PowerManagement/PowerManagement-637.20.2/pmset/pmset.c.auto.html | |
hat tip to frogor for the boilerplate :) | |
... but don't let any of this fool you: pmset -g rawlog is still way more efficient | |
... HOWEVER, pmset won't show you other interesting info emitted by this script! | |
Notes: | |
* "Selected adapter" and "Adapter Watts" may show stale info from the most recently | |
used adapter if the system is not charging (Apple Silicon only?) | |
* I don't know what some of the values mean, you'll just have to experiment for | |
yourself ;) | |
""" | |
IOKit_bundle = NSBundle.bundleWithIdentifier_("com.apple.framework.IOKit") | |
functions = [ | |
("IORegistryEntryFromPath", b"II*"), | |
("IORegistryEntryCreateCFProperties", b"IIo^@II"), | |
] | |
objc.loadBundleFunctions(IOKit_bundle, globals(), functions) | |
kIOMasterPortDefault = 0 | |
kCFAllocatorDefault = 0 | |
kNilOptions = 0 | |
# IOService:/AppleARMPE/arm-io/AppleT600xIO/smc@90400000/AppleASCWrapV4/iop-smc-nub/RTBuddy(SMC)/SMCEndpoint1/AppleSMCKeysEndpoint/AppleSmartBatteryManager/AppleSmartBattery | |
# M1 Max, maybe others | |
# ... it moved, maybe for Sonoma. This is the old one. | |
def oldM1MaxBatteryPath(): | |
return "/".join([ | |
'IOService:', 'AppleARMPE', 'arm-io', 'AppleT600xIO', | |
'smc@90400000', 'AppleASCWrapV4', 'iop-smc-nub', 'RTBuddyV2', | |
'SMCEndpoint1', 'AppleSMC', 'AppleSmartBatteryManager', | |
'AppleSmartBattery']).encode('utf-8') | |
# M1 Max, maybe others | |
def M1MaxBatteryPath(): | |
return "/".join([ | |
'IOService:', 'AppleARMPE', 'arm-io', 'AppleT600xIO', | |
'smc@90400000', 'AppleASCWrapV4', 'iop-smc-nub', 'RTBuddy(SMC)', | |
'SMCEndpoint1', 'AppleSMCKeysEndpoint', 'AppleSmartBatteryManager', | |
'AppleSmartBattery']).encode('utf-8') | |
# 2013 rMBP and a 2019 16" rMBP | |
def IntelBatteryPath(): | |
return "/".join([ | |
'IOService:', 'AppleACPIPlatformExpert', 'SMB0', | |
'AppleECSMBusController', 'AppleSmartBatteryManager', | |
'AppleSmartBattery']).encode('utf-8') | |
def M3MaxBatteryPath(): | |
return "/".join([ | |
'IOService:', | |
'AppleARMPE', | |
'arm-io@10F00000', | |
'AppleH15IO', | |
'smc@A4400000', | |
'AppleASCWrapV6', | |
'iop-smc-nub', | |
'RTBuddy(SMC)', | |
'SMCEndpoint1', | |
'AppleSMCKeysEndpoint', | |
'AppleSmartBatteryManager', | |
'AppleSmartBattery']).encode('utf-8') | |
def get_battery(path, entry): | |
err, details = IORegistryEntryCreateCFProperties( | |
entry, None, kCFAllocatorDefault, kNilOptions | |
) | |
return details, err | |
def validateIORegistry(paths): | |
""" | |
Check for battery info in the provided list of paths, stopping | |
at the first valid one. | |
""" | |
for path in paths: | |
#print(f"Trying {path}") | |
entry = IORegistryEntryFromPath(kIOMasterPortDefault, path) | |
# Is this IORegistry path valid? | |
res, err = get_battery(path, entry) | |
if err == 0: | |
return path, entry | |
print("Couldn't find a valid IO Registry path to battery info. Sorry!") | |
sys.exit(1) | |
path, entry = validateIORegistry([M3MaxBatteryPath(), oldM1MaxBatteryPath(), M1MaxBatteryPath(), IntelBatteryPath()]) | |
print( | |
""" | |
Displaying power consumption stats every minute. | |
Positive charge rate == charging | |
Negative charge rate == discharging. | |
"Time Remaining" means time until full when charging, or time until empty when discharging. | |
"Selected Adapter" is set to 254 when no power adapter is connected. | |
""" | |
) | |
# fmt = "{:<29} {:<7} {:<4} {:<10} {:<18} {:<18} {:<15} {:<2}" | |
fmt = "{:<17} {:<7} {:<4} {:<10} {:<18} {:<17} {:<15} {:<2}" | |
print( | |
fmt.format( | |
"Time", | |
"Rate", | |
"%", | |
"Capacity", | |
"Time Remaining", | |
# "Charging Voltage", | |
"Selected Adapter", | |
"Adapter Watts", | |
"Not Charging Reason", | |
) | |
) | |
while True: | |
res, err = get_battery(path, entry) | |
if err != 0: | |
print(f"Battery polling returned error {err}!") | |
# Uncomment the below line to see all the IORegistry data at this path | |
# print(res) | |
print( | |
fmt.format( | |
str(datetime.datetime.utcnow())[5:19], | |
res.get("Amperage") / 1000.0, | |
res.get("BatteryData").get("StateOfCharge"), | |
res.get("CurrentCapacity"), | |
res.get("TimeRemaining"), | |
# res["ChargerData"]["ChargingVoltage"], | |
res.get("BestAdapterIndex") or " ", | |
res.get("AdapterDetails").get("Watts") or " ", | |
res.get("ChargerData").get("NotChargingReason") or " ", | |
) | |
) | |
# IOKit provides new data at 60 second intervals | |
time.sleep(60) | |
Author
dreness
commented
Apr 13, 2020
Here's an M1 Max transitioning from battery to AC power.
Time Rate % Capacity Time Remaining Selected Adapter Adapter Watts Not Charging Reason
07-07 03:37:02 -0.528 19 20 171 254 128
07-07 03:38:02 8.17 19 20 252 3 140
07-07 03:39:02 8.527 21 21 98 3 140
07-07 03:40:02 8.551 23 23 94 3 140
07-07 03:41:02 8.544 25 25 92 3 140
07-07 03:42:02 8.544 26 27 90 3 140
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment