Skip to content

Instantly share code, notes, and snippets.

@dsbaars
Last active May 20, 2026 00:19
Show Gist options
  • Select an option

  • Save dsbaars/ced0d882a872dfbed12499b7811cf3c1 to your computer and use it in GitHub Desktop.

Select an option

Save dsbaars/ced0d882a872dfbed12499b7811cf3c1 to your computer and use it in GitHub Desktop.
smc-power: read USB-C adapter load + system power from Apple SMC on Linux (Apple Mac, T2 + pre-T2 supported)
#!/usr/bin/env python3
"""Show USB-C adapter load + system power via Apple SMC.
Auto-detects sysfs base (legacy applesmc.<N> on pre-T2, ACPI APP0001:00 on T2)
and SMC endianness (T2 stores ui/si little-endian; sp* and flt stay big- and
little-endian respectively on all Apple SMC variants).
Usage:
smc-power.py # one-shot
smc-power.py --watch # live every 1s (Ctrl-C to stop)
smc-power.py --raw KEY # dump one key's raw bytes (debug)
Requires the applesmc kernel module to be loaded.
"""
import argparse, struct, sys, time
from pathlib import Path
CANDIDATE_BASES = [
Path("/sys/devices/platform/applesmc.768"),
Path("/sys/bus/acpi/devices/APP0001:00"),
]
def find_base():
for p in CANDIDATE_BASES:
if (p / "key_at_index").exists():
return p
return None
class SMC:
def __init__(self, base):
self.base = base
self._idx = base / "key_at_index"
self._name = base / "key_at_index_name"
self._type = base / "key_at_index_type"
self._len = base / "key_at_index_data_length"
self._data = base / "key_at_index_data"
self.count = int((base / "key_count").read_text().strip())
self.cache = {}
def _read_at(self, i):
self._idx.write_text(str(i))
n = self._name.read_text().strip()
t = self._type.read_text().strip()
sz = int(self._len.read_text().strip())
d = self._data.read_bytes()[:sz]
return n, t, d
def refresh(self, names=None):
"""Walk all keys, fill cache. If names given, only cache those (faster)."""
wanted = set(names) if names else None
write_errors = 0
for i in range(self.count):
try:
name, typ, raw = self._read_at(i)
except OSError:
write_errors += 1
continue
if wanted is None or name in wanted:
self.cache[name] = (typ, raw)
if wanted is not None and len(self.cache) >= len(wanted):
return
if write_errors == self.count and not self.cache:
print(
"error: every applesmc key read failed (probably running inside an\n"
" unprivileged container where writes to key_at_index are denied).\n"
" Run this on the host, not inside the LXC/Docker namespace.",
file=sys.stderr,
)
sys.exit(4)
def raw(self, name):
return self.cache.get(name)
# Apple SMC stores integers (ui/si) big-endian on legacy Macs, little-endian
# on T2. Signed-point (sp) and float (flt) are always stored as Apple
# documents them (sp = big-endian fixed, flt = little-endian IEEE-754).
def autodetect_int_endian(smc):
"""Apple chargers report rated current `D1IR` between 1000 and 6000 mA
(5W .. ~120W at 20V). Whichever endian gives a value in that range is
the right one."""
pair = smc.raw("D1IR")
if pair is None:
return "big"
raw = pair[1]
big = int.from_bytes(raw, "big")
lit = int.from_bytes(raw, "little")
if 1000 <= big <= 6000:
return "big"
if 1000 <= lit <= 6000:
return "little"
return "big"
def decode(typ, raw, int_end):
if not raw:
return None
try:
if typ.startswith("ui"):
return int.from_bytes(raw, int_end, signed=False)
if typ.startswith("si"):
return int.from_bytes(raw, int_end, signed=True)
if typ.startswith("sp"):
fbits = int(typ[3], 16)
return int.from_bytes(raw, "big", signed=True) / (1 << fbits)
if typ.startswith("fp"):
fbits = int(typ[3], 16)
return int.from_bytes(raw, "big", signed=False) / (1 << fbits)
if typ.strip() == "flt":
return struct.unpack("<f", raw)[0]
if typ in ("ch8*", "char"):
return raw.split(b"\x00", 1)[0].decode("ascii", "replace")
return raw.hex()
except Exception as e:
return f"<err:{e}>"
# Keys we always want — SMC. Battery values are read from
# /sys/class/power_supply/BAT0 instead because the kernel driver decodes them
# correctly while the SMC layout for B* keys is endian-inconsistent on T2.
STATIC = ["D1im", "D1in", "D1is", "D1ih", "D1if", "D1ii", "D1IR", "D1VR"]
DYNAMIC = ["VD0R", "ID0R", "PD0R", "PSTR", "PCPT", "PPBR", "VP0R"]
ALL_WANTED = STATIC + DYNAMIC
def read_kernel_battery():
out = {}
p = Path("/sys/class/power_supply/BAT0/uevent")
if not p.exists():
return out
for line in p.read_text().splitlines():
if "=" in line:
k, v = line.split("=", 1)
out[k.replace("POWER_SUPPLY_", "")] = v
return out
def read_kernel_adapter():
p = Path("/sys/class/power_supply/ADP1/uevent")
if not p.exists():
return {}
out = {}
for line in p.read_text().splitlines():
if "=" in line:
k, v = line.split("=", 1)
out[k.replace("POWER_SUPPLY_", "")] = v
return out
def get(smc, end, name):
pair = smc.raw(name)
return decode(pair[0], pair[1], end) if pair else None
def fmt_val(v, ndig=2):
if v is None:
return " - "
if isinstance(v, float):
return f"{v:.{ndig}f}"
return str(v)
def report(smc, end, header=True):
if header:
print(f"# {time.strftime('%Y-%m-%d %H:%M:%S')} sysfs={smc.base} endian={end}/big/<f")
smc.refresh(ALL_WANTED)
ir = get(smc, end, "D1IR")
vr = get(smc, end, "D1VR")
rated_w = (ir or 0) * (vr or 0) / 1e6
print(f"Adapter: {get(smc, end, 'D1im')} {get(smc, end, 'D1in')}")
print(f" HW {get(smc, end, 'D1ih')} FW {get(smc, end, 'D1if')} "
f"ID {get(smc, end, 'D1ii')} S/N {get(smc, end, 'D1is')}")
print(f" Rated: {ir/1000:.2f} A @ {vr/1000:.2f} V = {rated_w:.0f} W"
if ir and vr else " (no rated info)")
print()
pd0r = get(smc, end, "PD0R")
util = 100.0 * pd0r / rated_w if rated_w and pd0r else None
bat = read_kernel_battery()
# kernel BAT0 reports voltage in µV and current in µA.
bat_v = int(bat["VOLTAGE_NOW"]) / 1_000_000 if bat.get("VOLTAGE_NOW") else None
bat_i_uA = int(bat.get("CURRENT_NOW", 0)) if bat.get("CURRENT_NOW") else 0
bat_status = bat.get("STATUS", "?")
bat_pct = bat.get("CAPACITY", "?")
bat_cycles = bat.get("CYCLE_COUNT", "?")
bat_temp = int(bat.get("TEMP", 0)) / 10 if bat.get("TEMP") else None
print(" value unit source")
rows = [
("Adapter voltage", get(smc, end, "VD0R"), "V", "smc VD0R"),
("Adapter current", get(smc, end, "ID0R"), "A", "smc ID0R"),
("Adapter power", pd0r, "W", "smc PD0R"),
("Adapter utilization", util, "%", "PD0R / rated"),
("System total", get(smc, end, "PSTR"), "W", "smc PSTR"),
("CPU package", get(smc, end, "PCPT"), "W", "smc PCPT"),
("Battery rail draw", get(smc, end, "PPBR"), "W", "smc PPBR"),
("Battery status", bat_status, "", "kernel BAT0"),
("Battery charge", bat_pct, "%", "kernel BAT0"),
("Battery voltage", bat_v, "V", "kernel BAT0"),
("Battery current", bat_i_uA / 1000 if bat_i_uA else 0, "mA", "kernel BAT0"),
("Battery temp", bat_temp, "°C", "kernel BAT0"),
("Battery cycles", bat_cycles, "", "kernel BAT0"),
]
for label, val, unit, src in rows:
print(f" {label:<22} {fmt_val(val):>8} {unit:<5} {src}")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--watch", action="store_true",
help="loop every 1s; clear screen each tick")
ap.add_argument("--raw", metavar="KEY",
help="dump one key's raw bytes (debug)")
args = ap.parse_args()
base = find_base()
if base is None:
print("error: applesmc sysfs not found — `modprobe applesmc` first",
file=sys.stderr)
sys.exit(2)
smc = SMC(base)
if args.raw:
smc.refresh([args.raw])
pair = smc.raw(args.raw)
if not pair:
print(f"{args.raw}: not found")
sys.exit(3)
typ, raw = pair
end = autodetect_int_endian(smc)
print(f"{args.raw} type={typ!r} bytes={raw.hex()} len={len(raw)} "
f"decoded={decode(typ, raw, end)!r}")
return
# endian probe uses D1IR which is rarely 0 on an Apple adapter
smc.refresh(["D1IR"])
end = autodetect_int_endian(smc)
if args.watch:
try:
while True:
print("\033[2J\033[H", end="")
report(smc, end)
time.sleep(1.0)
except KeyboardInterrupt:
pass
else:
report(smc, end)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment