refactor: sane-sysinfo: clean up a bit more

This commit is contained in:
Colin 2024-06-15 10:25:37 +00:00
parent df0a8cf900
commit a63f6281c5

View File

@ -68,21 +68,65 @@ class Formatter:
suffix_hr: str = SUFFIX_HR
suffix_min: str = SUFFIX_MIN
@dataclass
class ParsedPowerSupply:
class PowerSupply:
"""
near-direct values from /sys/class/power_supply API endpoints
reads values from /sys/class/power_supply/$dev/ API
"""
percent_charged: int | None = None
# unitless: could be joules, could be something else
charge_full: int | None = None
# charge per hour
charge_rate: int | None = None
def __init__(self, sysfs_node: str):
self.sysfs_node = sysfs_node
self._cached_reads = {}
def try_read(self, rel_path: str) -> str | None:
if rel_path not in self._cached_reads:
self._cached_reads[rel_path] = self.try_read_uncached(rel_path)
return self._cached_reads[rel_path]
def try_read_uncached(self, rel_path: str) -> str | None:
try:
v = open(f"{self.sysfs_node}/{rel_path}").read()
logger.debug(f"${self.sysfs_node}/{rel_path}: {v}")
return v
except:
return None
def try_read_int(self, rel_path: str) -> int | None:
s = self.try_read(rel_path)
if s is None: return None
return int(s)
@property
def capacity(self) -> int | None:
return self.try_read_int("capacity") # percent
@property
def charge_full_design(self) -> int | None:
return self.try_read_int("charge_full_design") # micro-Ah
@property
def current_now(self) -> int | None:
return self.try_read_int("current_now") # micro-A
@property
def energy_full(self) -> int | None:
return self.try_read_int("energy_full") # micro-Wh
# @property
# def energy_now(self) -> int | None:
# return self.try_read_int("energy_now") # micro-Wh
@property
def power_now(self) -> int | None:
return self.try_read_int("power_now") # micro-W (?)
@property
def status(self) -> str | None:
return self.try_read("status") # "Charging"/"Discharging"/"Not charging"
# @property
# def voltage_now(self) -> int | None:
# return self.try_read_int("voltage_now") # micro-V
def is_valid(self) -> bool:
return self.percent_charged is not None and \
self.charge_full is not None and \
self.charge_rate is not None
class BatteryInfo:
"""
@ -92,21 +136,28 @@ class BatteryInfo:
minutes_to_charged: int | None = None
minutes_to_discharged: int | None = None
def __init__(self, p: ParsedPowerSupply):
assert p.percent_charged is not None
self.percent_charged = p.percent_charged
def __init__(self, capacity: int, charges_per_hour: float | None, status: str | None):
self.percent_charged = capacity
if p.charge_rate is not None and p.charge_rate < 0:
# correct some batteries which flip signs in places
if status.lower().strip() == "charging" and charges_per_hour:
charges_per_hour = abs(charges_per_hour)
logger.debug("status==charging => forcing charges_per_hour positive")
elif status.lower().strip() == "discharging" and charges_per_hour:
charges_per_hour = -abs(charges_per_hour)
logger.debug("status==discharging => forcing charges_per_hour negative")
if charges_per_hour is not None and charges_per_hour < 0:
self.minutes_to_discharged = int(
60
* p.charge_full * self.percent_charged/100
/ -p.charge_rate
* self.percent_charged/100
/ -charges_per_hour
)
if p.charge_full is not None and p.charge_rate is not None and p.charge_rate > 0:
if charges_per_hour is not None and charges_per_hour > 0:
self.minutes_to_charged = int(
60
* p.charge_full * (100-self.percent_charged)/100
/ p.charge_rate * 60
* (100-self.percent_charged)/100
/ charges_per_hour
)
@ -124,56 +175,24 @@ def render_icon(direction: ChargeDirection, percentage: float) -> str:
raise RuntimeError(f"invalid ChargeDirection {direction}")
def try_path(p: str) -> ParsedPowerSupply | None:
def try_path(p: str) -> BatteryInfo | None:
"""
try to read battery information from some p = "/sys/class/power_supply/$node" path
"""
state = ParsedPowerSupply()
try:
capacity_text = open(f"{p}/capacity").read()
except: pass
else:
logger.debug(f"read from {p}/capacity: {capacity_text}")
state.percent_charged = int(capacity_text)
ps = PowerSupply(p)
if ps.capacity is None:
return None
try:
charge_full_design_text = open(f"{p}/charge_full_design").read()
current_now_text = open(f"{p}/current_now").read()
except: pass
else:
logger.debug(f"read from {p}/charge_full_design: {charge_full_design_text}")
logger.debug(f"read from {p}/charge_now: {charge_now_text}")
state.charge_full = int(charge_full_design_text)
if ps.charge_full_design and ps.current_now is not None:
# current_now is positive when charging
state.charge_rate = int(current_now_text)
if state.is_valid(): return state
try:
energy_full_text = open(f"{p}/energy_full").read()
power_now_text = open(f"{p}/power_now").read()
except: pass
else:
logger.debug(f"read from {p}/energy_full: {energy_full_text}")
logger.debug(f"read from {p}/power_now: {power_now_text}")
state.charge_full = int(energy_full_text)
charges_per_hour = ps.current_now / ps.charge_full_design
elif ps.energy_full and ps.power_now is not None:
# power_now is positive when discharging
state.charge_rate = -int(power_now_text)
if state.is_valid(): return state
try:
energy_full_text = open(f"{p}/energy_full").read()
energy_now_text = open(f"{p}/energy_now").read()
except: pass
charges_per_hour = -ps.power_now / ps.energy_full
else:
logger.debug(f"read from {p}/energy_full: {energy_full_text}")
logger.debug(f"read from {p}/energy_now: {energy_now_text}")
state.charge_full = int(energy_full_text)
# energy_now is positive when discharging
state.charge_rate = -int(energy_now_text)
charges_per_hour = None
return state if state.percent_charged is not None else None
return BatteryInfo(ps.capacity, charges_per_hour, ps.status)
def try_all_paths() -> BatteryInfo | None:
p = try_path("/sys/class/power_supply/axp20x-battery") # Pinephone
@ -181,11 +200,10 @@ def try_all_paths() -> BatteryInfo | None:
p = try_path("/sys/class/power_supply/BAT0") # Thinkpad
logger.debug(f"perc: {p.percent_charged if p else None}")
logger.debug(f"full: {p.charge_full if p else None}, rate: {p.charge_rate if p else None}")
logger.debug(" rate > 0 means charging, else discharging")
logger.debug(f"charge: {p.percent_charged if p else None}")
logger.debug(f"min-to-charge: {p.minutes_to_charged if p else None}, min-to-discharge: {p.minutes_to_discharged if p else None}")
if p.percent_charged is not None:
return BatteryInfo(p)
return p
def fmt_minutes(f: Formatter, icon: str, if_indefinite: str, minutes: int | None) -> str:
logger.debug(f"charge/discharge time: {minutes} min")