dashboard/app/collect/power.py

73 lines
2.1 KiB
Python

import glob
from app.sample import Sample
_PS = "/sys/class/power_supply"
def _read(path: str) -> str | None:
"""Read a sysfs file, returning its stripped contents.
Args:
path: path under /sys.
Returns:
The file contents, or None if it cannot be read.
"""
try:
with open(path) as f:
return f.read().strip()
except OSError:
return None
def _supplies() -> list[tuple[str, str]]:
"""List power supplies found under /sys/class/power_supply.
Returns:
(type, path) pairs sorted by path, where type is the sysfs type
("battery", "mains", "usb", ...) of each supply.
"""
out: list[tuple[str, str]] = []
for p in sorted(glob.glob(f"{_PS}/*")):
t = _read(f"{p}/type")
if t:
out.append((t.lower(), p))
return out
def fill(s: Sample) -> None:
"""Fill the battery / ac_online fields of a Sample from sysfs.
psutil's battery API is unreliable here (power_plugged can be None),
so /sys/class/power_supply/* is read directly: the first present
battery provides capacity and status, and ac_online becomes True when
any mains — or, failing that, USB — supply reports online. Fields stay
at their Sample defaults on a desktop without these nodes.
Args:
s: sample to fill.
"""
try:
supplies = _supplies()
for t, p in supplies:
if t == "battery" and _read(f"{p}/present") == "1":
cap = _read(f"{p}/capacity")
if cap is not None:
try:
s.battery = int(cap)
except ValueError:
pass
s.battery_status = _read(f"{p}/status")
break
for t, p in supplies:
if t == "mains" and _read(f"{p}/online") == "1":
s.ac_online = True
break
if s.ac_online is None:
for t, p in supplies:
if t == "usb" and _read(f"{p}/online") == "1":
s.ac_online = True
break
except OSError:
pass