Skip to content

Instantly share code, notes, and snippets.

@dreness
Last active January 31, 2024 02:41
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dreness/c83813479c458f4a274d755b967fc973 to your computer and use it in GitHub Desktop.
Save dreness/c83813479c458f4a274d755b967fc973 to your computer and use it in GitHub Desktop.
Display rate of charge / discharge of an Apple laptop battery
#!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)
@dreness
Copy link
Author

dreness commented Jul 7, 2022

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