sane-sysinfo: port to Python
it's a pretty literal port; probably has some bugs
This commit is contained in:
parent
c50a4d1d71
commit
a4f5343fb5
|
@ -1,5 +1,5 @@
|
|||
{ static-nix-shell }:
|
||||
static-nix-shell.mkBash {
|
||||
static-nix-shell.mkPython3Bin {
|
||||
pname = "sane-sysinfo";
|
||||
srcRoot = ./.;
|
||||
}
|
||||
|
|
|
@ -1,183 +1,229 @@
|
|||
#!/usr/bin/env nix-shell
|
||||
#!nix-shell -i bash -p bash
|
||||
#!nix-shell -i python3 -p "python3.withPackages (ps: [ ])"
|
||||
"""
|
||||
usage: sane-sysinfo [options...]
|
||||
|
||||
usage() {
|
||||
echo "usage: sane-sysinfo [options...]"
|
||||
echo
|
||||
echo "pretty-prints a battery estimate (icon to indicate state, and a duration estimate)"
|
||||
echo
|
||||
echo "options:"
|
||||
echo " --debug: output additional information, to stderr"
|
||||
echo " --minute-suffix <string>: use the provided string as a minutes suffix"
|
||||
echo " --hour-suffix <string>: use the provided string as an hours suffix"
|
||||
echo " --icon-suffix <string>: use the provided string as an icon suffix"
|
||||
echo " --percent-suffix <string>: use the provided string when displaying percents"
|
||||
}
|
||||
pretty-prints a battery estimate (icon to indicate state, and a duration estimate)
|
||||
|
||||
options:
|
||||
--debug: output additional information, to stderr
|
||||
--minute-suffix <string>: use the provided string as a minutes suffix
|
||||
--hour-suffix <string>: use the provided string as an hours suffix
|
||||
--icon-suffix <string>: use the provided string as an icon suffix
|
||||
--percent-suffix <string>: use the provided string when displaying percents
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# these icons may only render in nerdfonts
|
||||
icon_bat_chg=("" "" "" "")
|
||||
icon_bat_dis=("" "" "" "")
|
||||
suffix_icon=" " # thin space
|
||||
suffix_percent="%"
|
||||
# suffix_icon=" "
|
||||
ICON_BAT_CHG = ["", "", "", ""]
|
||||
ICON_BAT_DIS = ["", "", "", ""]
|
||||
SUFFIX_ICON = " " # thin space
|
||||
SUFFIX_PERCENT = "%"
|
||||
# SUFFIX_ICON=" "
|
||||
|
||||
# render time like: 2ʰ08ᵐ
|
||||
# unicode sub/super-scripts: <https://en.wikipedia.org/wiki/Unicode_subscripts_and_superscripts>
|
||||
# symbol_hr="ʰ"
|
||||
# symbol_min="ᵐ"
|
||||
# SUFFIX_HR="ʰ"
|
||||
# SUFFIX_MIN="ᵐ"
|
||||
|
||||
# render time like: 2ₕ08ₘ
|
||||
# symbol_hr="ₕ"
|
||||
# symbol_min="ₘ"
|
||||
# SUFFIX_HR="ₕ"
|
||||
# SUFFIX_MIN="ₘ"
|
||||
|
||||
# render time like: 2h08m
|
||||
# symbol_hr="h"
|
||||
# symbol_min="m"
|
||||
# SUFFIX_HR="H"
|
||||
# SUFFIX_MIN="M"
|
||||
|
||||
# render time like: 2:08
|
||||
# symbol_hr=":"
|
||||
# symbol_min=
|
||||
# SUFFIX_HR=":"
|
||||
# SUFFIX_MIN=
|
||||
|
||||
# render time like: 2꞉08⧗
|
||||
symbol_hr="꞉"
|
||||
symbol_min="⧗"
|
||||
SUFFIX_HR="꞉"
|
||||
SUFFIX_MIN="⧗"
|
||||
# variants:
|
||||
# symbol_hr=":"
|
||||
# symbol_min="⧖"
|
||||
# symbol_min="⌛"
|
||||
# SUFFIX_HR=":"
|
||||
# SUFFIX_MIN="⧖"
|
||||
# SUFFIX_MIN="⌛"
|
||||
|
||||
# render time like: 2'08"
|
||||
# symbol_hr="'"
|
||||
# symbol_min='"'
|
||||
# SUFFIX_HR="'"
|
||||
# SUFFIX_MIN='"'
|
||||
|
||||
log() {
|
||||
if [ "$BATTERY_ESTIMATE_DEBUG" = "1" ]; then
|
||||
printf "$@" >&2
|
||||
echo >&2
|
||||
fi
|
||||
}
|
||||
class ChargeDirection(Enum):
|
||||
Charging = "Charging"
|
||||
Discharging = "Discharging"
|
||||
|
||||
render_icon() {
|
||||
# args:
|
||||
# 1: "chg" or "dis"
|
||||
# 2: current battery percentage
|
||||
level=$(($2 / 25))
|
||||
level=$(($level > 3 ? 3 : $level))
|
||||
level=$(($level < 0 ? 0 : $level))
|
||||
log "icon: %s %d" "$1" "$level"
|
||||
if [ "$1" = "dis" ]; then
|
||||
printf "%s" "${icon_bat_dis[$level]}"
|
||||
elif [ "$1" = "chg" ]; then
|
||||
printf "%s" "${icon_bat_chg[$level]}"
|
||||
fi
|
||||
}
|
||||
@dataclass
|
||||
class Formatter:
|
||||
suffix_icon: str = SUFFIX_ICON
|
||||
suffix_percent: str = SUFFIX_PERCENT
|
||||
suffix_hr: str = SUFFIX_HR
|
||||
suffix_min: str = SUFFIX_MIN
|
||||
|
||||
try_path() {
|
||||
# assigns output variables:
|
||||
# - perc, perc_from_full (0-100)
|
||||
# - full, rate (pos means charging)
|
||||
if [ -f "$1/capacity" ]; then
|
||||
log "perc, perc_from_full from %s" "$1/capacity"
|
||||
perc=$(cat "$1/capacity")
|
||||
perc_from_full=$((100 - $perc))
|
||||
fi
|
||||
@dataclass
|
||||
class ParsedPowerSupply:
|
||||
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
|
||||
|
||||
if [ -f "$1/charge_full_design" ] && [ -f "$1/current_now" ]; then
|
||||
log "full, rate from %s and %s" "$1/charge_full_design" "$1/current_now"
|
||||
# current is positive when charging
|
||||
full=$(cat "$1/charge_full_design")
|
||||
rate=$(cat "$1/current_now")
|
||||
elif [ -f "$1/energy_full" ] && [ -f "$1/power_now" ]; then
|
||||
log "full, rate from %s and %s" "$1/energy_full" "$1/power_now"
|
||||
# power_now is positive when discharging
|
||||
full=$(cat "$1/energy_full")
|
||||
rate=-$(cat "$1/power_now")
|
||||
elif [ -f "$1/energy_full" ] && [ -f "$1/energy_now" ]; then
|
||||
log "full, rate from %s and %s" "$1/energy_full" "$1/energy_now"
|
||||
log " this is a compatibility path for legacy Thinkpad batteries which do not populate the 'power_now' field, and incorrectly populate 'energy_now' with power info"
|
||||
# energy_now is positive when discharging
|
||||
full=$(cat "$1/energy_full")
|
||||
rate=-$(cat "$1/energy_now")
|
||||
fi
|
||||
}
|
||||
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
|
||||
|
||||
try_all_paths() {
|
||||
try_path "/sys/class/power_supply/axp20x-battery" # Pinephone
|
||||
try_path "/sys/class/power_supply/BAT0" # Thinkpad
|
||||
log "perc: %d, perc_from_full: %d" "$perc" "$perc_from_full"
|
||||
log "full: %f, rate: %f" "$full" "$rate"
|
||||
log " rate > 0 means charging, else discharging"
|
||||
}
|
||||
@property
|
||||
def percent_discharged(self) -> int | None:
|
||||
if self.percent_charged is not None:
|
||||
return 100 - self.percent_charged
|
||||
|
||||
fmt_minutes() {
|
||||
# args:
|
||||
# 1: icon to render
|
||||
# 2: string to show if charge/discharge time is indefinite
|
||||
# 3: minutes to stable state (i.e. to full charge or full discharge)
|
||||
# - we work in minutes instead of hours for precision: bash math is integer-only
|
||||
log "charge/discharge time: %f min" "$3"
|
||||
# args: <battery symbol> <text if ludicrous estimate> <estimated minutes to full/empty>
|
||||
if [ -n "$3" ] && [ "$3" -lt 1440 ]; then
|
||||
hr=$(($3 / 60))
|
||||
hr_in_min=$(($hr * 60))
|
||||
min=$(($3 - $hr_in_min))
|
||||
printf "%s%s%d%s%02d%s" "$1" "$suffix_icon" "$hr" "$symbol_hr" "$min" "$symbol_min"
|
||||
else
|
||||
log "charge/discharge duration > 1d"
|
||||
printf "%s%s%s" "$1" "$suffix_icon" "$2" # more than 1d
|
||||
fi
|
||||
}
|
||||
@property
|
||||
def discharge_rate(self) -> int | None:
|
||||
if self.charge_rate is not None:
|
||||
return -self.charge_rate
|
||||
|
||||
pretty_output() {
|
||||
if [ -n "$perc" ]; then
|
||||
duration=""
|
||||
if [ "$rate" -gt 0 ]; then
|
||||
log "charging"
|
||||
icon="$(render_icon chg $perc)"
|
||||
duration="$(($full * 60 * $perc_from_full / (100 * $rate)))"
|
||||
else
|
||||
log "discharging"
|
||||
icon="$(render_icon dis $perc)"
|
||||
if [ "$rate" -lt 0 ]; then
|
||||
duration="$(($full * 60 * $perc / (-100 * $rate)))"
|
||||
fi
|
||||
fi
|
||||
fmt_minutes "$icon" "$perc$suffix_percent" "$duration"
|
||||
fi
|
||||
}
|
||||
@property
|
||||
def minutes_to_charged(self) -> int | None:
|
||||
if self.percent_discharged is not None and self.charge_full and self.charge_rate > 0:
|
||||
return int(self.charge_full * self.percent_discharged/100 / self.charge_rate * 60)
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
"--debug")
|
||||
shift
|
||||
BATTERY_ESTIMATE_DEBUG=1
|
||||
;;
|
||||
"--icon-suffix")
|
||||
shift
|
||||
suffix_icon="$1"
|
||||
shift
|
||||
;;
|
||||
"--hour-suffix")
|
||||
shift
|
||||
symbol_hr="$1"
|
||||
shift
|
||||
;;
|
||||
"--minute-suffix")
|
||||
shift
|
||||
symbol_min="$1"
|
||||
shift
|
||||
;;
|
||||
"--percent-suffix")
|
||||
shift
|
||||
suffix_percent="$1"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
@property
|
||||
def minutes_to_discharged(self) -> int | None:
|
||||
if self.percent_charged is not None and self.charge_full and self.charge_rate < 0:
|
||||
return int(self.charge_full * self.percent_charged/100 / self.discharge_rate * 60)
|
||||
|
||||
try_all_paths
|
||||
pretty_output
|
||||
|
||||
def render_icon(direction: ChargeDirection, percentage: float) -> str:
|
||||
level = percentage / 25
|
||||
level = max(0, min(3, level))
|
||||
level = int(round(level))
|
||||
|
||||
logger.debug(f"render_icon: direction={direction} level={level}")
|
||||
|
||||
if direction == ChargeDirection.Charging:
|
||||
return ICON_BAT_CHG[level]
|
||||
elif direction == ChargeDirection.Discharging:
|
||||
return ICON_BAT_DIS[level]
|
||||
|
||||
raise RuntimeError(f"invalid ChargeDirection {direction}")
|
||||
|
||||
def try_path(p: str) -> ParsedPowerSupply | 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)
|
||||
|
||||
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)
|
||||
# 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)
|
||||
# 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
|
||||
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)
|
||||
|
||||
return state if state.percent_charged is not None else None
|
||||
|
||||
def try_all_paths() -> ParsedPowerSupply | None:
|
||||
p = try_path("/sys/class/power_supply/axp20x-battery") # Pinephone
|
||||
if p is 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")
|
||||
|
||||
return p
|
||||
|
||||
def fmt_minutes(f: Formatter, icon: str, if_indefinite: str, minutes: int | None) -> str:
|
||||
logger.debug(f"charge/discharge time: {minutes} min")
|
||||
|
||||
if minutes < 1440:
|
||||
hr = minutes // 60
|
||||
min = minutes % 60
|
||||
return f"{icon}{f.suffix_icon}{hr}{f.suffix_hr}{min:02}{f.suffix_min}"
|
||||
else:
|
||||
logger.debug("charge/discharge duration > 1d")
|
||||
return f"{icon}{f.suffix_icon}{if_indefinite}"
|
||||
|
||||
def pretty_output(f: Formatter, p: ParsedPowerSupply) -> str:
|
||||
if p.charge_rate > 0:
|
||||
logger.debug("charging")
|
||||
icon = render_icon(ChargeDirection.Charging, p.percent_charged)
|
||||
duration = p.minutes_to_charged
|
||||
else:
|
||||
logger.debug("discharging")
|
||||
icon = render_icon(ChargeDirection.Discharging, p.percent_discharged)
|
||||
duration = p.minutes_to_discharged
|
||||
|
||||
return fmt_minutes(f, icon, f"{p.percent_charged}{f.suffix_percent}", duration)
|
||||
|
||||
def main():
|
||||
logging.basicConfig()
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
|
||||
parser = argparse.ArgumentParser(usage=__doc__)
|
||||
parser.add_argument("--debug", action="store_true")
|
||||
parser.add_argument("--icon-suffix", default=SUFFIX_ICON)
|
||||
parser.add_argument("--hour-suffix", default=SUFFIX_HR)
|
||||
parser.add_argument("--minute-suffix", default=SUFFIX_MIN)
|
||||
parser.add_argument("--percent-suffix", default=SUFFIX_PERCENT)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.debug:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
f = Formatter()
|
||||
f.suffix_icon = args.icon_suffix
|
||||
f.suffix_percent = args.percent_suffix
|
||||
f.suffix_hr = args.hour_suffix
|
||||
f.suffix_min = args.minute_suffix
|
||||
|
||||
p = try_all_paths()
|
||||
print(pretty_output(f, p))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
Loading…
Reference in New Issue
Block a user