| #!/usr/bin/env python3 |
| """ |
| poly-battery-status-py: Generates a pretty status-bar string for multi-battery systems on Linux. |
| Copyright (C) 2020 Falke Carlsen |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU General Public License as published by |
| the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License |
| along with this program. If not, see <https://www.gnu.org/licenses/>. |
| """ |
| |
| import re |
| import sys |
| from enum import Enum |
| from pathlib import Path |
| |
| PSEUDO_FS_PATH = "/sys/class/power_supply/" |
| CURRENT_CHARGE_FILENAME = "energy_now" |
| MAX_CHARGE_FILENAME = "energy_full" |
| POWER_DRAW_FILENAME = "power_now" |
| TLP_THRESHOLD_PERCENTAGE = 1.0 |
| PERCENTAGE_FORMAT = ".2%" |
| |
| if len(sys.argv) > 1: |
| # parsing threshold |
| try: |
| TLP_THRESHOLD_PERCENTAGE = float(sys.argv[1]) |
| except ValueError: |
| print(f"[ERROR]: Could not convert '{sys.argv[1]}' into a float.") |
| if len(sys.argv) > 2: |
| # parsing formatting |
| PERCENTAGE_FORMAT = sys.argv[2] |
| |
| |
| class Status(Enum): |
| CHARGING = 1 |
| DISCHARGING = 2 |
| PASSIVE = 3 |
| |
| |
| class Configuration: |
| time_to_completion: int |
| percentage: float |
| status: Status |
| |
| def __init__(self, time_to_completion, percentage, status): |
| self.time_to_completion = time_to_completion |
| self.percentage = percentage |
| self.status = status |
| |
| |
| class Battery: |
| status: Status |
| current_charge: int |
| max_charge: int |
| power_draw: int |
| |
| def __init__(self, status, current_charge, max_charge, power_draw): |
| self.Status = status |
| self.current_charge = current_charge |
| self.max_charge = max_charge |
| self.power_draw = power_draw |
| |
| |
| def get_configuration() -> Configuration: |
| # get all batteries on system |
| batteries = [] |
| for x in Path(PSEUDO_FS_PATH).iterdir(): |
| bat_name = str(x.parts[len(x.parts) - 1]) |
| if re.match("^BAT\d+$", bat_name): |
| batteries.append(Battery( |
| get_status(bat_name), |
| get_current_charge(bat_name), |
| get_max_charge(bat_name), |
| get_power_draw(bat_name))) |
| |
| # calculate global status, assumes that if a battery is not passive, it will be discharging or charging |
| config_status = Status.PASSIVE |
| for bat in batteries: |
| if bat.Status == Status.CHARGING: |
| config_status = Status.CHARGING |
| break |
| elif bat.Status == Status.DISCHARGING: |
| config_status = Status.DISCHARGING |
| break |
| |
| # construct and return configuration |
| return Configuration(calc_time(batteries, config_status), calc_percentage(batteries), config_status) |
| |
| |
| def get_status(bat_name: str) -> Status: |
| raw_status = Path(f"{PSEUDO_FS_PATH}{bat_name}/status").open().read().strip() |
| if raw_status == "Unknown" or raw_status == "Full": |
| return Status.PASSIVE |
| elif raw_status == "Charging": |
| return Status.CHARGING |
| elif raw_status == "Discharging": |
| return Status.DISCHARGING |
| else: |
| raise ValueError |
| |
| |
| def get_current_charge(bat_name: str) -> int: |
| return int(Path(f"{PSEUDO_FS_PATH}{bat_name}/{CURRENT_CHARGE_FILENAME}").open().read().strip()) |
| |
| |
| def get_max_charge(bat_name: str) -> int: |
| return int(Path(f"{PSEUDO_FS_PATH}{bat_name}/{MAX_CHARGE_FILENAME}").open().read().strip()) |
| |
| |
| def get_power_draw(bat_name: str) -> int: |
| return int(Path(f"{PSEUDO_FS_PATH}{bat_name}/{POWER_DRAW_FILENAME}").open().read().strip()) |
| |
| |
| def calc_time(batteries: list, status: Status) -> int: |
| if status == Status.PASSIVE: |
| return 0 |
| # get total metrics on configuration |
| total_current_charge = sum([bat.current_charge for bat in batteries]) |
| total_max_charge = sum([bat.max_charge for bat in batteries]) |
| total_power_draw = sum([bat.power_draw for bat in batteries]) |
| if total_power_draw == 0: |
| return 0 |
| if status == Status.DISCHARGING: |
| # return number of seconds until empty |
| return (total_current_charge / total_power_draw) * 3600 |
| elif status == Status.CHARGING: |
| # return number of seconds until (optionally relatively) charged |
| return (((total_max_charge * TLP_THRESHOLD_PERCENTAGE) - total_current_charge) / total_power_draw) * 3600 |
| |
| |
| def calc_percentage(batteries: list) -> float: |
| total_max_charge = sum([bat.max_charge for bat in batteries]) |
| total_current_charge = sum([bat.current_charge for bat in batteries]) |
| return total_current_charge / total_max_charge |
| |
| |
| def calc_display_time(status: Status, seconds: int) -> str: |
| hours = int(seconds // 3600) |
| minutes = int((seconds % 3600) / 60) |
| if status == Status.PASSIVE: |
| return "" |
| |
| # assume charging initially if not passive |
| direction = "+" |
| if status == Status.DISCHARGING: |
| direction = "-" |
| |
| # format output digitally, e.g. (+0:09) |
| return f" ({direction}{hours}:{minutes:02})" |
| |
| |
| def print_status(config: Configuration): |
| print(f"{config.percentage:{PERCENTAGE_FORMAT}}{calc_display_time(config.status, config.time_to_completion)}") |
| |
| |
| def main(): |
| print_status(get_configuration()) |
| |
| |
| if __name__ == '__main__': |
| main() |