refactor: sane-sysinfo: clean up a bit more

This commit is contained in:
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_hr: str = SUFFIX_HR
suffix_min: str = SUFFIX_MIN suffix_min: str = SUFFIX_MIN
@dataclass class PowerSupply:
class ParsedPowerSupply:
""" """
near-direct values from /sys/class/power_supply API endpoints reads values from /sys/class/power_supply/$dev/ API
""" """
percent_charged: int | None = None def __init__(self, sysfs_node: str):
# unitless: could be joules, could be something else self.sysfs_node = sysfs_node
charge_full: int | None = None self._cached_reads = {}
# charge per hour
charge_rate: int | None = None 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: class BatteryInfo:
""" """
@@ -92,21 +136,28 @@ class BatteryInfo:
minutes_to_charged: int | None = None minutes_to_charged: int | None = None
minutes_to_discharged: int | None = None minutes_to_discharged: int | None = None
def __init__(self, p: ParsedPowerSupply): def __init__(self, capacity: int, charges_per_hour: float | None, status: str | None):
assert p.percent_charged is not None self.percent_charged = capacity
self.percent_charged = p.percent_charged
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( self.minutes_to_discharged = int(
60 60
* p.charge_full * self.percent_charged/100 * self.percent_charged/100
/ -p.charge_rate / -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( self.minutes_to_charged = int(
60 60
* p.charge_full * (100-self.percent_charged)/100 * (100-self.percent_charged)/100
/ p.charge_rate * 60 / charges_per_hour
) )
@@ -124,56 +175,24 @@ def render_icon(direction: ChargeDirection, percentage: float) -> str:
raise RuntimeError(f"invalid ChargeDirection {direction}") 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 try to read battery information from some p = "/sys/class/power_supply/$node" path
""" """
state = ParsedPowerSupply() ps = PowerSupply(p)
try: if ps.capacity is None:
capacity_text = open(f"{p}/capacity").read() return None
except: pass
else:
logger.debug(f"read from {p}/capacity: {capacity_text}")
state.percent_charged = int(capacity_text)
try: if ps.charge_full_design and ps.current_now is not None:
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)
# current_now is positive when charging # current_now is positive when charging
state.charge_rate = int(current_now_text) charges_per_hour = ps.current_now / ps.charge_full_design
elif ps.energy_full and ps.power_now is not None:
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)
# power_now is positive when discharging # power_now is positive when discharging
state.charge_rate = -int(power_now_text) charges_per_hour = -ps.power_now / ps.energy_full
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
else: else:
logger.debug(f"read from {p}/energy_full: {energy_full_text}") charges_per_hour = None
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)
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: def try_all_paths() -> BatteryInfo | None:
p = try_path("/sys/class/power_supply/axp20x-battery") # Pinephone 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 p = try_path("/sys/class/power_supply/BAT0") # Thinkpad
logger.debug(f"perc: {p.percent_charged if p else None}") 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(f"charge: {p.percent_charged if p else None}")
logger.debug(" rate > 0 means charging, else discharging") 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 p
return BatteryInfo(p)
def fmt_minutes(f: Formatter, icon: str, if_indefinite: str, minutes: int | None) -> str: def fmt_minutes(f: Formatter, icon: str, if_indefinite: str, minutes: int | None) -> str:
logger.debug(f"charge/discharge time: {minutes} min") logger.debug(f"charge/discharge time: {minutes} min")