import bisect
import configparser
import os
import re
from i3pystatus import IntervalModule, formatp
from i3pystatus.core.command import run_through_shell
from i3pystatus.core.desktop import DesktopNotification
from i3pystatus.core.util import lchop, TimeWrapper, make_bar, make_glyph, make_vertical_bar
class UEventParser(configparser.ConfigParser):
@staticmethod
def parse_file(file):
parser = UEventParser()
with open(file, "rb") as file:
parser.read_string(file.read().decode(errors="replace"))
return dict(parser.items("id10t"))
def __init__(self):
super().__init__(default_section="id10t")
def optionxform(self, key):
return lchop(key, "POWER_SUPPLY_")
def read_string(self, string):
super().read_string("[id10t]\n" + string)
class Battery:
@staticmethod
def create(from_file):
battery_info = UEventParser.parse_file(from_file)
if "POWER_NOW" in battery_info:
return BatteryEnergy(battery_info)
else:
return BatteryCharge(battery_info)
def __init__(self, battery_info):
self.battery_info = battery_info
self.normalize_micro()
def normalize_micro(self):
for key, micro_value in self.battery_info.items():
if re.match(r"(VOLTAGE|CHARGE|CURRENT|POWER|ENERGY)_(NOW|FULL|MIN)(_DESIGN)?", key):
self.battery_info[key] = float(micro_value) / 1000000.0
def percentage(self, design=False):
return self._percentage("_DESIGN" if design else "") * 100
def status(self):
if self.consumption() is None:
return self.battery_info["STATUS"]
elif self.consumption() > 0.1 and self.percentage() < 99.9:
return "Discharging" if self.battery_info["STATUS"] == "Discharging" else "Charging"
elif self.consumption() == 0 and self.percentage() == 0.00:
return "Depleted"
else:
return "Full"
def consumption(self, val):
return val if val > 0.1 else 0
class BatteryCharge(Battery):
def __init__(self, bi):
bi["CHARGE_FULL"] = bi["CHARGE_FULL_DESIGN"] if bi["CHARGE_NOW"] > bi["CHARGE_FULL"] else bi["CHARGE_FULL"]
super().__init__(bi)
def consumption(self):
if "VOLTAGE_NOW" in self.battery_info and "CURRENT_NOW" in self.battery_info:
return super().consumption(self.battery_info["VOLTAGE_NOW"] * abs(self.battery_info["CURRENT_NOW"])) # V * A = W
else:
return None
def _percentage(self, design):
return self.battery_info["CHARGE_NOW"] / self.battery_info["CHARGE_FULL" + design]
def wh_remaining(self):
return self.battery_info['CHARGE_NOW'] * self.battery_info['VOLTAGE_NOW']
def wh_total(self):
return self.battery_info['CHARGE_FULL'] * self.battery_info['VOLTAGE_NOW']
def wh_depleted(self):
return (self.battery_info['CHARGE_FULL'] - self.battery_info['CHARGE_NOW']) * self.battery_info['VOLTAGE_NOW']
def remaining(self):
if self.status() == "Discharging":
if "CHARGE_NOW" in self.battery_info and "CURRENT_NOW" in self.battery_info:
# Ah / A = h * 60 min = min
return self.battery_info["CHARGE_NOW"] / self.battery_info["CURRENT_NOW"] * 60
else:
return -1
else:
return (self.battery_info["CHARGE_FULL"] - self.battery_info["CHARGE_NOW"]) / self.battery_info[
"CURRENT_NOW"] * 60
class BatteryEnergy(Battery):
def consumption(self):
return super().consumption(self.battery_info["POWER_NOW"])
def _percentage(self, design):
return self.battery_info["ENERGY_NOW"] / self.battery_info["ENERGY_FULL" + design]
def wh_remaining(self):
return self.battery_info['ENERGY_NOW']
def wh_total(self):
return self.battery_info['ENERGY_FULL']
def wh_depleted(self):
return self.battery_info['ENERGY_FULL'] - self.battery_info['ENERGY_NOW']
def remaining(self):
if self.status() == "Discharging":
# Wh / W = h * 60 min = min
return self.battery_info["ENERGY_NOW"] / self.battery_info["POWER_NOW"] * 60
else:
return (self.battery_info["ENERGY_FULL"] - self.battery_info["ENERGY_NOW"]) / self.battery_info[
"POWER_NOW"] * 60
[docs]class BatteryChecker(IntervalModule):
"""
This class uses the /sys/class/power_supply/…/uevent interface to check for the
battery status.
Setting ``battery_ident`` to ``ALL`` will summarise all available batteries
and aggregate the % as well as the time remaining on the charge. This is
helpful when the machine has more than one battery available.
.. rubric:: Available formatters
* `{remaining}` — remaining time for charging or discharging, uses TimeWrapper formatting, default format is `%E%h:%M`
* `{percentage}` — battery percentage relative to the last full value
* `{percentage_design}` — absolute battery charge percentage
* `{consumption (Watts)}` — current power flowing into/out of the battery
* `{status}`
* `{no_of_batteries}` — The number of batteries included
* `{battery_ident}` — the same as the setting
* `{bar}` —bar displaying the relative percentage graphically
* `{bar_design}` —bar displaying the absolute percentage graphically
* `{glyph}` — A single character or string (selected from 'glyphs') representing the current battery percentage
This module supports the :ref:`formatp <formatp>` extended string format
syntax. By setting the ``FULL`` status to an empty string, and including
brackets around the ``{status}`` formatter, the text within the brackets
will be hidden when the battery is full, as can be seen in the below
example:
.. code-block:: python
from i3pystatus import Status
status = Status()
status.register(
'battery',
interval=5,
format='{battery_ident}: [{status} ]{percentage_design:.2f}%',
alert=True,
alert_percentage=15,
status={
'DPL': 'DPL',
'CHR': 'CHR',
'DIS': 'DIS',
'FULL': '',
}
)
# status.register(
# 'battery',
# format='{status} {percentage:.0f}%',
# levels={
# 25: "<=25",
# 50: "<=50",
# 75: "<=75",
# },
# )
status.run()
"""
settings = (
("battery_ident", "The name of your battery, usually BAT0 or BAT1"),
"format",
("not_present_text", "Text displayed if the battery is not present. No formatters are available"),
("alert", "Display a libnotify-notification on low battery"),
("critical_level_command", "Runs a shell command in the case of a critical power state"),
"critical_level_percentage",
"alert_percentage",
"alert_timeout",
("alert_format_title", "The title of the notification, all formatters can be used"),
("alert_format_body", "The body text of the notification, all formatters can be used"),
("path", "Override the default-generated path and specify the full path for a single battery"),
("base_path", "Override the default base path for searching for batteries"),
("battery_prefix", "Override the default battery prefix"),
("status", "A dictionary mapping ('DPL', 'DIS', 'CHR', 'FULL') to alternative names"),
("levels", "A dictionary mapping percentages of charge levels to corresponding names."),
("color", "The text color"),
("full_color", "The full color"),
("charging_color", "The charging color"),
("critical_color", "The critical color"),
("not_present_color", "The not present color."),
("not_present_text",
"The text to display when the battery is not present. Provides {battery_ident} as formatting option"),
("no_text_full", "Don't display text when battery is full - 100%"),
("glyphs", "Arbitrarily long string of characters (or array of strings) to represent battery charge percentage"),
("use_design_percentage", "Use design percentage rather then absolute percentage for alerts")
)
battery_ident = "ALL"
format = "{status} {remaining}"
status = {
"DPL": "DPL",
"CHR": "CHR",
"DIS": "DIS",
"FULL": "FULL",
}
levels = None
not_present_text = "Battery {battery_ident} not present"
alert = False
critical_level_command = ""
critical_level_percentage = 1
alert_percentage = 10
alert_timeout = -1
alert_format_title = "Low battery"
alert_format_body = "Battery {battery_ident} has only {percentage:.2f}% ({remaining:%E%hh:%Mm}) remaining!"
color = "#ffffff"
full_color = "#00ff00"
charging_color = "#00ff00"
critical_color = "#ff0000"
not_present_color = "#ffffff"
no_text_full = False
glyphs = "▁▂▃▄▅▆▇█"
use_design_percentage = False
battery_prefix = 'BAT'
base_path = '/sys/class/power_supply'
path = None
paths = []
notification = None
def percentage(self, batteries, design=False):
total_now = [battery.wh_remaining() for battery in batteries]
total_full = [battery.wh_total() for battery in batteries]
return sum(total_now) / sum(total_full) * 100
def consumption(self, batteries):
consumption = 0
for battery in batteries:
if battery.consumption() is not None:
consumption += battery.consumption()
return consumption
def abs_consumption(self, batteries):
abs_consumption = 0
for battery in batteries:
if battery.consumption() is None:
continue
if battery.status() == 'Discharging':
abs_consumption -= battery.consumption()
elif battery.status() == 'Charging':
abs_consumption += battery.consumption()
return abs_consumption
def battery_status(self, batteries):
abs_consumption = self.abs_consumption(batteries)
if abs_consumption > 0:
return 'Charging'
elif abs_consumption < 0:
return 'Discharging'
else:
return batteries[-1].status()
def remaining(self, batteries):
wh_depleted = 0
wh_remaining = 0
abs_consumption = self.abs_consumption(batteries)
for battery in batteries:
wh_remaining += battery.wh_remaining()
wh_depleted += battery.wh_depleted()
if abs_consumption == 0:
return 0
elif abs_consumption > 0:
return wh_depleted / self.consumption(batteries) * 60
elif abs_consumption < 0:
return wh_remaining / self.consumption(batteries) * 60
def init(self):
if not self.paths or (self.path and self.path not in self.paths):
bat_dir = self.base_path
if os.path.exists(bat_dir) and not self.path:
_, dirs, _ = next(os.walk(bat_dir))
all_bats = [x for x in dirs if x.startswith(self.battery_prefix)]
for bat in all_bats:
self.paths.append(os.path.join(bat_dir, bat, 'uevent'))
if self.path:
self.paths = [self.path]
def run(self):
urgent = False
color = self.color
batteries = []
for path in self.paths:
if self.battery_ident == 'ALL' or path.find(self.battery_ident) >= 0:
try:
batteries.append(Battery.create(path))
except FileNotFoundError:
pass
if not batteries:
format_dict = {'battery_ident': self.battery_ident}
self.output = {
"full_text": formatp(self.not_present_text, **format_dict),
"color": self.not_present_color,
}
return
if self.no_text_full:
if self.battery_status(batteries) == "Full":
self.output = {
"full_text": ""
}
return
fdict = {
"battery_ident": self.battery_ident,
"no_of_batteries": len(batteries),
"percentage": self.percentage(batteries),
"percentage_design": self.percentage(batteries, design=True),
"consumption": self.consumption(batteries),
"remaining": TimeWrapper(0, "%E%h:%M"),
"glyph": make_glyph(self.percentage(batteries), self.glyphs),
"bar": make_bar(self.percentage(batteries)),
"bar_design": make_bar(self.percentage(batteries, design=True)),
"vertical_bar": make_vertical_bar(self.percentage(batteries)),
"vertical_bar_design": make_vertical_bar(self.percentage(batteries, design=True)),
}
status = self.battery_status(batteries)
if status in ["Charging", "Discharging"]:
remaining = self.remaining(batteries)
fdict["remaining"] = TimeWrapper(remaining * 60, "%E%h:%M")
if status == "Discharging":
fdict["status"] = "DIS"
if self.percentage(batteries) <= self.alert_percentage:
urgent = True
color = self.critical_color
else:
fdict["status"] = "CHR"
color = self.charging_color
elif status == 'Depleted':
fdict["status"] = "DPL"
color = self.critical_color
else:
fdict["status"] = "FULL"
color = self.full_color
if self.critical_level_command and fdict["status"] == "DIS" and fdict["percentage"] <= self.critical_level_percentage:
run_through_shell(self.critical_level_command, enable_shell=True)
self.alert_if_low_battery(fdict)
if self.levels and fdict['status'] == 'DIS':
self.levels.setdefault(0, self.status.get('DPL', 'DPL'))
self.levels.setdefault(100, self.status.get('FULL', 'FULL'))
keys = sorted(self.levels.keys())
index = bisect.bisect_left(keys, int(fdict['percentage']))
fdict["status"] = self.levels[keys[index]]
else:
fdict["status"] = self.status[fdict["status"]]
self.data = fdict
self.output = {
"full_text": formatp(self.format, **fdict),
"instance": self.battery_ident,
"urgent": urgent,
"color": color,
}
def alert_if_low_battery(self, fdict):
if self.use_design_percentage:
percentage = fdict['percentage_design']
else:
percentage = fdict['percentage']
if self.alert and fdict["status"] == "DIS" and percentage <= self.alert_percentage:
title, body = formatp(self.alert_format_title, **fdict), formatp(self.alert_format_body, **fdict)
if self.notification is None:
self.notification = DesktopNotification(
title=title,
body=body,
icon="battery-caution",
urgency=2,
timeout=self.alert_timeout,
)
self.notification.display()
else:
self.notification.update(title=title,
body=body)