blob: a16bb67f180006b064829f3e112a4c1e379249c9 [file] [log] [blame]
Copybara botca5ce642024-11-08 17:38:08 +01001#!/usr/bin/env python3
2"""
3poly-battery-status-py: Generates a pretty status-bar string for multi-battery systems on Linux.
4Copyright (C) 2020 Falke Carlsen
5
6This program is free software: you can redistribute it and/or modify
7it under the terms of the GNU General Public License as published by
8the Free Software Foundation, either version 3 of the License, or
9(at your option) any later version.
10
11This program is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14GNU General Public License for more details.
15
16You should have received a copy of the GNU General Public License
17along with this program. If not, see <https://www.gnu.org/licenses/>.
18"""
19
20import re
21import sys
22from enum import Enum
23from pathlib import Path
24
25PSEUDO_FS_PATH = "/sys/class/power_supply/"
26CURRENT_CHARGE_FILENAME = "energy_now"
27MAX_CHARGE_FILENAME = "energy_full"
28POWER_DRAW_FILENAME = "power_now"
29TLP_THRESHOLD_PERCENTAGE = 1.0
30PERCENTAGE_FORMAT = ".2%"
31
32if 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
43class Status(Enum):
44 CHARGING = 1
45 DISCHARGING = 2
46 PASSIVE = 3
47
48
49class 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
60class 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
73def 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
99def 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
111def 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
115def 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
119def 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
123def 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
140def 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
146def 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
161def print_status(config: Configuration):
162 print(f"{config.percentage:{PERCENTAGE_FORMAT}}{calc_display_time(config.status, config.time_to_completion)}")
163
164
165def main():
166 print_status(get_configuration())
167
168
169if __name__ == '__main__':
170 main()