Copybara bot | ca5ce64 | 2024-11-08 17:38:08 +0100 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | """ |
| 3 | poly-battery-status-py: Generates a pretty status-bar string for multi-battery systems on Linux. |
| 4 | Copyright (C) 2020 Falke Carlsen |
| 5 | |
| 6 | This program is free software: you can redistribute it and/or modify |
| 7 | it under the terms of the GNU General Public License as published by |
| 8 | the Free Software Foundation, either version 3 of the License, or |
| 9 | (at your option) any later version. |
| 10 | |
| 11 | This program is distributed in the hope that it will be useful, |
| 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | GNU General Public License for more details. |
| 15 | |
| 16 | You should have received a copy of the GNU General Public License |
| 17 | along with this program. If not, see <https://www.gnu.org/licenses/>. |
| 18 | """ |
| 19 | |
| 20 | import re |
| 21 | import sys |
| 22 | from enum import Enum |
| 23 | from pathlib import Path |
| 24 | |
| 25 | PSEUDO_FS_PATH = "/sys/class/power_supply/" |
| 26 | CURRENT_CHARGE_FILENAME = "energy_now" |
| 27 | MAX_CHARGE_FILENAME = "energy_full" |
| 28 | POWER_DRAW_FILENAME = "power_now" |
| 29 | TLP_THRESHOLD_PERCENTAGE = 1.0 |
| 30 | PERCENTAGE_FORMAT = ".2%" |
| 31 | |
| 32 | if len(sys.argv) > 1: |
| 33 | # parsing threshold |
| 34 | try: |
| 35 | TLP_THRESHOLD_PERCENTAGE = float(sys.argv[1]) |
| 36 | except ValueError: |
| 37 | print(f"[ERROR]: Could not convert '{sys.argv[1]}' into a float.") |
| 38 | if len(sys.argv) > 2: |
| 39 | # parsing formatting |
| 40 | PERCENTAGE_FORMAT = sys.argv[2] |
| 41 | |
| 42 | |
| 43 | class Status(Enum): |
| 44 | CHARGING = 1 |
| 45 | DISCHARGING = 2 |
| 46 | PASSIVE = 3 |
| 47 | |
| 48 | |
| 49 | class Configuration: |
| 50 | time_to_completion: int |
| 51 | percentage: float |
| 52 | status: Status |
| 53 | |
| 54 | def __init__(self, time_to_completion, percentage, status): |
| 55 | self.time_to_completion = time_to_completion |
| 56 | self.percentage = percentage |
| 57 | self.status = status |
| 58 | |
| 59 | |
| 60 | class Battery: |
| 61 | status: Status |
| 62 | current_charge: int |
| 63 | max_charge: int |
| 64 | power_draw: int |
| 65 | |
| 66 | def __init__(self, status, current_charge, max_charge, power_draw): |
| 67 | self.Status = status |
| 68 | self.current_charge = current_charge |
| 69 | self.max_charge = max_charge |
| 70 | self.power_draw = power_draw |
| 71 | |
| 72 | |
| 73 | def get_configuration() -> Configuration: |
| 74 | # get all batteries on system |
| 75 | batteries = [] |
| 76 | for x in Path(PSEUDO_FS_PATH).iterdir(): |
| 77 | bat_name = str(x.parts[len(x.parts) - 1]) |
| 78 | if re.match("^BAT\d+$", bat_name): |
| 79 | batteries.append(Battery( |
| 80 | get_status(bat_name), |
| 81 | get_current_charge(bat_name), |
| 82 | get_max_charge(bat_name), |
| 83 | get_power_draw(bat_name))) |
| 84 | |
| 85 | # calculate global status, assumes that if a battery is not passive, it will be discharging or charging |
| 86 | config_status = Status.PASSIVE |
| 87 | for bat in batteries: |
| 88 | if bat.Status == Status.CHARGING: |
| 89 | config_status = Status.CHARGING |
| 90 | break |
| 91 | elif bat.Status == Status.DISCHARGING: |
| 92 | config_status = Status.DISCHARGING |
| 93 | break |
| 94 | |
| 95 | # construct and return configuration |
| 96 | return Configuration(calc_time(batteries, config_status), calc_percentage(batteries), config_status) |
| 97 | |
| 98 | |
| 99 | def get_status(bat_name: str) -> Status: |
| 100 | raw_status = Path(f"{PSEUDO_FS_PATH}{bat_name}/status").open().read().strip() |
| 101 | if raw_status == "Unknown" or raw_status == "Full": |
| 102 | return Status.PASSIVE |
| 103 | elif raw_status == "Charging": |
| 104 | return Status.CHARGING |
| 105 | elif raw_status == "Discharging": |
| 106 | return Status.DISCHARGING |
| 107 | else: |
| 108 | raise ValueError |
| 109 | |
| 110 | |
| 111 | def get_current_charge(bat_name: str) -> int: |
| 112 | return int(Path(f"{PSEUDO_FS_PATH}{bat_name}/{CURRENT_CHARGE_FILENAME}").open().read().strip()) |
| 113 | |
| 114 | |
| 115 | def get_max_charge(bat_name: str) -> int: |
| 116 | return int(Path(f"{PSEUDO_FS_PATH}{bat_name}/{MAX_CHARGE_FILENAME}").open().read().strip()) |
| 117 | |
| 118 | |
| 119 | def get_power_draw(bat_name: str) -> int: |
| 120 | return int(Path(f"{PSEUDO_FS_PATH}{bat_name}/{POWER_DRAW_FILENAME}").open().read().strip()) |
| 121 | |
| 122 | |
| 123 | def calc_time(batteries: list, status: Status) -> int: |
| 124 | if status == Status.PASSIVE: |
| 125 | return 0 |
| 126 | # get total metrics on configuration |
| 127 | total_current_charge = sum([bat.current_charge for bat in batteries]) |
| 128 | total_max_charge = sum([bat.max_charge for bat in batteries]) |
| 129 | total_power_draw = sum([bat.power_draw for bat in batteries]) |
| 130 | if total_power_draw == 0: |
| 131 | return 0 |
| 132 | if status == Status.DISCHARGING: |
| 133 | # return number of seconds until empty |
| 134 | return (total_current_charge / total_power_draw) * 3600 |
| 135 | elif status == Status.CHARGING: |
| 136 | # return number of seconds until (optionally relatively) charged |
| 137 | return (((total_max_charge * TLP_THRESHOLD_PERCENTAGE) - total_current_charge) / total_power_draw) * 3600 |
| 138 | |
| 139 | |
| 140 | def calc_percentage(batteries: list) -> float: |
| 141 | total_max_charge = sum([bat.max_charge for bat in batteries]) |
| 142 | total_current_charge = sum([bat.current_charge for bat in batteries]) |
| 143 | return total_current_charge / total_max_charge |
| 144 | |
| 145 | |
| 146 | def calc_display_time(status: Status, seconds: int) -> str: |
| 147 | hours = int(seconds // 3600) |
| 148 | minutes = int((seconds % 3600) / 60) |
| 149 | if status == Status.PASSIVE: |
| 150 | return "" |
| 151 | |
| 152 | # assume charging initially if not passive |
| 153 | direction = "+" |
| 154 | if status == Status.DISCHARGING: |
| 155 | direction = "-" |
| 156 | |
| 157 | # format output digitally, e.g. (+0:09) |
| 158 | return f" ({direction}{hours}:{minutes:02})" |
| 159 | |
| 160 | |
| 161 | def print_status(config: Configuration): |
| 162 | print(f"{config.percentage:{PERCENTAGE_FORMAT}}{calc_display_time(config.status, config.time_to_completion)}") |
| 163 | |
| 164 | |
| 165 | def main(): |
| 166 | print_status(get_configuration()) |
| 167 | |
| 168 | |
| 169 | if __name__ == '__main__': |
| 170 | main() |