arcobattery.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. from __future__ import division
  2. import cairocffi
  3. import os
  4. from libqtile import bar
  5. from libqtile.widget import base
  6. from pathlib import Path
  7. BAT_NAME = ""
  8. #configure the name of the battery automatically
  9. config = Path('/sys/class/power_supply/BAT0')
  10. if config.is_dir():
  11. BAT_NAME = "BAT0"
  12. config = Path('/sys/class/power_supply/BAT1')
  13. if config.is_dir():
  14. BAT_NAME = "BAT1"
  15. config = Path('/sys/class/power_supply/BAT2')
  16. if config.is_dir():
  17. BAT_NAME = "BAT2"
  18. #Navigate to /sys/class/power_supply to check what the name is of your battery
  19. #Type it in manually
  20. #BAT_NAME = "..."
  21. BAT_DIR = '/sys/class/power_supply'
  22. CHARGED = 'Full'
  23. CHARGING = 'Charging'
  24. DISCHARGING = 'Discharging'
  25. UNKNOWN = 'Unknown'
  26. BATTERY_INFO_FILES = {
  27. 'energy_now_file': ['energy_now', 'charge_now'],
  28. 'energy_full_file': ['energy_full', 'charge_full'],
  29. 'power_now_file': ['power_now', 'current_now'],
  30. 'status_file': ['status'],
  31. }
  32. def default_icon_path():
  33. # default icons are in libqtile/resources/battery-icons
  34. root = os.sep.join(os.path.abspath(__file__).split(os.sep)[:-2])
  35. return os.path.join(root, 'resources', 'battery-icons')
  36. class _Battery(base._TextBox):
  37. """Base battery class"""
  38. filenames = {}
  39. defaults = [
  40. ('battery_name', BAT_NAME, 'ACPI name of a battery, usually BAT0'),
  41. (
  42. 'status_file',
  43. 'status',
  44. 'Name of status file in'
  45. ' /sys/class/power_supply/battery_name'
  46. ),
  47. (
  48. 'energy_now_file',
  49. None,
  50. 'Name of file with the '
  51. 'current energy in /sys/class/power_supply/battery_name'
  52. ),
  53. (
  54. 'energy_full_file',
  55. None,
  56. 'Name of file with the maximum'
  57. ' energy in /sys/class/power_supply/battery_name'
  58. ),
  59. (
  60. 'power_now_file',
  61. None,
  62. 'Name of file with the current'
  63. ' power draw in /sys/class/power_supply/battery_name'
  64. ),
  65. ('update_delay', 60, 'The delay in seconds between updates'),
  66. ]
  67. def __init__(self, **config):
  68. base._TextBox.__init__(self, "BAT", bar.CALCULATED, **config)
  69. self.add_defaults(_Battery.defaults)
  70. def _load_file(self, name):
  71. try:
  72. path = os.path.join(BAT_DIR, self.battery_name, name)
  73. with open(path, 'r') as f:
  74. return f.read().strip()
  75. except IOError:
  76. if name == 'current_now':
  77. return 0
  78. return False
  79. except Exception:
  80. self.log.exception("Failed to get %s" % name)
  81. def _get_param(self, name):
  82. if name in self.filenames and self.filenames[name]:
  83. return self._load_file(self.filenames[name])
  84. elif name not in self.filenames:
  85. # Don't have the file name cached, figure it out
  86. # Don't modify the global list! Copy with [:]
  87. file_list = BATTERY_INFO_FILES.get(name, [])[:]
  88. if getattr(self, name, None):
  89. # If a file is manually specified, check it first
  90. file_list.insert(0, getattr(self, name))
  91. # Iterate over the possibilities, and return the first valid value
  92. for file in file_list:
  93. value = self._load_file(file)
  94. if value is not False and value is not None:
  95. self.filenames[name] = file
  96. return value
  97. # If we made it this far, we don't have a valid file.
  98. # Set it to None to avoid trying the next time.
  99. self.filenames[name] = None
  100. return None
  101. def _get_info(self):
  102. try:
  103. info = {
  104. 'stat': self._get_param('status_file'),
  105. 'now': float(self._get_param('energy_now_file')),
  106. 'full': float(self._get_param('energy_full_file')),
  107. 'power': float(self._get_param('power_now_file')),
  108. }
  109. except TypeError:
  110. return False
  111. return info
  112. class Battery(_Battery):
  113. """
  114. A simple but flexible text-based battery widget.
  115. """
  116. orientations = base.ORIENTATION_HORIZONTAL
  117. defaults = [
  118. ('charge_char', '^', 'Character to indicate the battery is charging'),
  119. ('discharge_char',
  120. 'V',
  121. 'Character to indicate the battery is discharging'
  122. ),
  123. ('error_message', 'Error', 'Error message if something is wrong'),
  124. ('format',
  125. '{char} {percent:2.0%} {hour:d}:{min:02d}',
  126. 'Display format'
  127. ),
  128. ('hide_threshold', None, 'Hide the text when there is enough energy'),
  129. ('low_percentage',
  130. 0.10,
  131. "Indicates when to use the low_foreground color 0 < x < 1"
  132. ),
  133. ('low_foreground', 'FF0000', 'Font color on low battery'),
  134. ]
  135. def __init__(self, **config):
  136. _Battery.__init__(self, **config)
  137. self.add_defaults(Battery.defaults)
  138. def timer_setup(self):
  139. update_delay = self.update()
  140. if update_delay is None and self.update_delay is not None:
  141. self.timeout_add(self.update_delay, self.timer_setup)
  142. elif update_delay:
  143. self.timeout_add(update_delay, self.timer_setup)
  144. def _configure(self, qtile, bar):
  145. if self.configured:
  146. self.update()
  147. _Battery._configure(self, qtile, bar)
  148. def _get_text(self):
  149. info = self._get_info()
  150. if info is False:
  151. return self.error_message
  152. # Set the charging character
  153. try:
  154. # hide the text when it's higher than threshold, but still
  155. # display `full` when the battery is fully charged.
  156. if self.hide_threshold and \
  157. info['now'] / info['full'] * 100.0 >= \
  158. self.hide_threshold and \
  159. info['stat'] != CHARGED:
  160. return ''
  161. elif info['stat'] == DISCHARGING:
  162. char = self.discharge_char
  163. time = info['now'] / info['power']
  164. elif info['stat'] == CHARGING:
  165. char = self.charge_char
  166. time = (info['full'] - info['now']) / info['power']
  167. else:
  168. return 'Full'
  169. except ZeroDivisionError:
  170. time = -1
  171. # Calculate the battery percentage and time left
  172. if time >= 0:
  173. hour = int(time)
  174. min = int(time * 60) % 60
  175. else:
  176. hour = -1
  177. min = -1
  178. percent = info['now'] / info['full']
  179. if info['stat'] == DISCHARGING and percent < self.low_percentage:
  180. self.layout.colour = self.low_foreground
  181. else:
  182. self.layout.colour = self.foreground
  183. return self.format.format(
  184. char=char,
  185. percent=percent,
  186. hour=hour,
  187. min=min
  188. )
  189. def update(self):
  190. ntext = self._get_text()
  191. if ntext != self.text:
  192. self.text = ntext
  193. self.bar.draw()
  194. class BatteryIcon(_Battery):
  195. """Battery life indicator widget."""
  196. orientations = base.ORIENTATION_HORIZONTAL
  197. defaults = [
  198. ('theme_path', default_icon_path(), 'Path of the icons'),
  199. ('custom_icons', {}, 'dict containing key->filename icon map'),
  200. ("scaleadd", 0, "Enable/Disable image scaling"),
  201. ("y_poss", 0, "Modify y possition"),
  202. ]
  203. def __init__(self, **config):
  204. _Battery.__init__(self, **config)
  205. self.add_defaults(BatteryIcon.defaults)
  206. # self.scale = 1.0 / self.scale
  207. if self.theme_path:
  208. self.length_type = bar.STATIC
  209. self.length = 0
  210. self.surfaces = {}
  211. self.current_icon = 'battery-missing'
  212. self.icons = dict([(x, '{0}.png'.format(x)) for x in (
  213. 'battery-missing',
  214. 'battery-empty',
  215. 'battery-empty-charge',
  216. 'battery-10',
  217. 'battery-10-charge',
  218. 'battery-20',
  219. 'battery-20-charge',
  220. 'battery-30',
  221. 'battery-30-charge',
  222. 'battery-40',
  223. 'battery-40-charge',
  224. 'battery-50',
  225. 'battery-50-charge',
  226. 'battery-60',
  227. 'battery-60-charge',
  228. 'battery-70',
  229. 'battery-70-charge',
  230. 'battery-80',
  231. 'battery-80-charge',
  232. 'battery-90',
  233. 'battery-90-charge',
  234. 'battery-full',
  235. 'battery-full-charge',
  236. 'battery-full-charged',
  237. )])
  238. self.icons.update(self.custom_icons)
  239. def timer_setup(self):
  240. self.update()
  241. self.timeout_add(self.update_delay, self.timer_setup)
  242. def _configure(self, qtile, bar):
  243. base._TextBox._configure(self, qtile, bar)
  244. self.setup_images()
  245. def _get_icon_key(self):
  246. key = 'battery'
  247. info = self._get_info()
  248. if info is False or not info.get('full'):
  249. key += '-missing'
  250. else:
  251. percent = info['now'] / info['full']
  252. if percent < .1:
  253. key += '-empty'
  254. elif percent < .2:
  255. key += '-10'
  256. elif percent < .3:
  257. key += '-20'
  258. elif percent < .4:
  259. key += '-30'
  260. elif percent < .5:
  261. key += '-40'
  262. elif percent < .6:
  263. key += '-50'
  264. elif percent < .5:
  265. key += '-60'
  266. elif percent < .8:
  267. key += '-70'
  268. elif percent < .9:
  269. key += '-80'
  270. elif percent < 1:
  271. key += '-90'
  272. else:
  273. key += '-full'
  274. if info['stat'] == CHARGING:
  275. key += '-charge'
  276. elif info['stat'] == CHARGED:
  277. key += '-charged'
  278. return key
  279. def update(self):
  280. icon = self._get_icon_key()
  281. if icon != self.current_icon:
  282. self.current_icon = icon
  283. self.draw()
  284. def draw(self):
  285. if self.theme_path:
  286. self.drawer.clear(self.background or self.bar.background)
  287. self.drawer.ctx.set_source(self.surfaces[self.current_icon])
  288. self.drawer.ctx.paint()
  289. self.drawer.draw(offsetx=self.offset, width=self.length)
  290. else:
  291. self.text = self.current_icon[8:]
  292. base._TextBox.draw(self)
  293. def setup_images(self):
  294. for key, name in self.icons.items():
  295. try:
  296. path = os.path.join(self.theme_path, name)
  297. img = cairocffi.ImageSurface.create_from_png(path)
  298. except cairocffi.Error:
  299. self.theme_path = None
  300. self.qtile.log.warning('Battery Icon switching to text mode')
  301. return
  302. input_width = img.get_width()
  303. input_height = img.get_height()
  304. sp = input_height / (self.bar.height - 1)
  305. width = input_width / sp
  306. if width > self.length:
  307. self.length = int(width) + self.actual_padding * 2
  308. imgpat = cairocffi.SurfacePattern(img)
  309. scaler = cairocffi.Matrix()
  310. scaler.scale(sp, sp)
  311. scaler.scale(self.scale, self.scale)
  312. factor = (1 - 1 / self.scale) / 2
  313. scaler.translate(-width * factor, -width * factor)
  314. scaler.translate(self.actual_padding * -1, self.y_poss)
  315. imgpat.set_matrix(scaler)
  316. imgpat.set_filter(cairocffi.FILTER_BEST)
  317. self.surfaces[key] = imgpat