summaryrefslogtreecommitdiffstats
path: root/powerline/segments/common/bat.py
blob: c892f62adcf8f01bcc21b99a5ac0d1c05467e1af (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# vim:fileencoding=utf-8:noet
from __future__ import (unicode_literals, division, absolute_import, print_function)

import os
import sys
import re

from powerline.lib.shell import run_cmd


def _fetch_battery_info(pl):
	try:
		import dbus
	except ImportError:
		pl.debug('Not using DBUS+UPower as dbus is not available')
	else:
		try:
			bus = dbus.SystemBus()
		except Exception as e:
			pl.exception('Failed to connect to system bus: {0}', str(e))
		else:
			interface = 'org.freedesktop.UPower'
			try:
				up = bus.get_object(interface, '/org/freedesktop/UPower')
			except dbus.exceptions.DBusException as e:
				if getattr(e, '_dbus_error_name', '').endswith('ServiceUnknown'):
					pl.debug('Not using DBUS+UPower as UPower is not available via dbus')
				else:
					pl.exception('Failed to get UPower service with dbus: {0}', str(e))
			else:
				devinterface = 'org.freedesktop.DBus.Properties'
				devtype_name = interface + '.Device'
				devices = []
				for devpath in up.EnumerateDevices(dbus_interface=interface):
					dev = bus.get_object(interface, devpath)
					devget = lambda what: dev.Get(
						devtype_name,
						what,
						dbus_interface=devinterface
					)
					if int(devget('Type')) != 2:
						pl.debug('Not using DBUS+UPower with {0}: invalid type', devpath)
						continue
					if not bool(devget('IsPresent')):
						pl.debug('Not using DBUS+UPower with {0}: not present', devpath)
						continue
					if not bool(devget('PowerSupply')):
						pl.debug('Not using DBUS+UPower with {0}: not a power supply', devpath)
						continue
					devices.append(devpath)
					pl.debug('Using DBUS+UPower with {0}', devpath)
				if devices:
					def _flatten_battery(pl):
						energy = 0.0
						energy_full = 0.0
						state = True
						for devpath in devices:
							dev = bus.get_object(interface, devpath)
							energy_full += float(
								dbus.Interface(dev, dbus_interface=devinterface).Get(
									devtype_name,
									'EnergyFull'
								),
							)
							energy += float(
								dbus.Interface(dev, dbus_interface=devinterface).Get(
									devtype_name,
									'Energy'
								),
							)
							state &= dbus.Interface(dev, dbus_interface=devinterface).Get(
								devtype_name,
								'State'
							) != 2
						if energy_full > 0:
							return (energy * 100.0 / energy_full), state
						else:
							return 0.0, state
					return _flatten_battery
				pl.debug('Not using DBUS+UPower as no batteries were found')

	if os.path.isdir('/sys/class/power_supply'):
		# ENERGY_* attributes represents capacity in µWh only.
		# CHARGE_* attributes represents capacity in µAh only.
		linux_capacity_units = ('energy', 'charge')
		linux_energy_full_fmt = '/sys/class/power_supply/{0}/{1}_full'
		linux_energy_fmt = '/sys/class/power_supply/{0}/{1}_now'
		linux_status_fmt = '/sys/class/power_supply/{0}/status'
		devices = []
		for linux_supplier in os.listdir('/sys/class/power_supply'):
			for unit in linux_capacity_units:
				energy_path = linux_energy_fmt.format(linux_supplier, unit)
				if not os.path.exists(energy_path):
					continue
				pl.debug('Using /sys/class/power_supply with battery {0} and unit {1}',
					linux_supplier, unit)
				devices.append((linux_supplier, unit))
				break  # energy or charge, not both
		if devices:
			def _get_battery_status(pl):
				energy = 0.0
				energy_full = 0.0
				state = True
				for device, unit in devices:
					with open(linux_energy_full_fmt.format(device, unit), 'r') as f:
						energy_full += int(float(f.readline().split()[0]))
					with open(linux_energy_fmt.format(device, unit), 'r') as f:
						energy += int(float(f.readline().split()[0]))
					try:
						with open(linux_status_fmt.format(device), 'r') as f:
							state &= (f.readline().strip() != 'Discharging')
					except IOError:
						state = None
				return (energy * 100.0 / energy_full), state
			return _get_battery_status
			pl.debug('Not using /sys/class/power_supply as no batteries were found')
		else:
			pl.debug("Checking for first capacity battery percentage")
			for batt in os.listdir('/sys/class/power_supply'):
				if os.path.exists('/sys/class/power_supply/{0}/capacity'.format(batt)):
					def _get_battery_perc(pl):
						state = True
						with open('/sys/class/power_supply/{0}/capacity'.format(batt), 'r') as f:
							perc = int(f.readline().split()[0])
						try:
							with open(linux_status_fmt.format(batt), 'r') as f:
								state &= (f.readline().strip() != 'Discharging')
						except IOError:
							state = None
						return perc, state
					return _get_battery_perc
	else:
		pl.debug('Not using /sys/class/power_supply: no directory')

	try:
		from shutil import which  # Python-3.3 and later
	except ImportError:
		pl.info('Using dumb “which” which only checks for file in /usr/bin')
		which = lambda f: (lambda fp: os.path.exists(fp) and fp)(os.path.join('/usr/bin', f))

	if which('pmset'):
		pl.debug('Using pmset')

		BATTERY_PERCENT_RE = re.compile(r'(\d+)%')

		def _get_battery_status(pl):
			battery_summary = run_cmd(pl, ['pmset', '-g', 'batt'])
			battery_percent = BATTERY_PERCENT_RE.search(battery_summary).group(1)
			ac_charging = 'AC' in battery_summary
			return int(battery_percent), ac_charging
		return _get_battery_status
	else:
		pl.debug('Not using pmset: executable not found')

	if sys.platform.startswith('win') or sys.platform == 'cygwin':
		# From http://stackoverflow.com/a/21083571/273566, reworked
		try:
			from win32com.client import GetObject
		except ImportError:
			pl.debug('Not using win32com.client as it is not available')
		else:
			try:
				wmi = GetObject('winmgmts:')
			except Exception as e:
				pl.exception('Failed to run GetObject from win32com.client: {0}', str(e))
			else:
				for battery in wmi.InstancesOf('Win32_Battery'):
					pl.debug('Using win32com.client with Win32_Battery')

					def _get_battery_status(pl):
						# http://msdn.microsoft.com/en-us/library/aa394074(v=vs.85).aspx
						return battery.EstimatedChargeRemaining, battery.BatteryStatus == 6

					return _get_battery_status
				pl.debug('Not using win32com.client as no batteries were found')
		from ctypes import Structure, c_byte, c_ulong, byref
		if sys.platform == 'cygwin':
			pl.debug('Using cdll to communicate with kernel32 (Cygwin)')
			from ctypes import cdll
			library_loader = cdll
		else:
			pl.debug('Using windll to communicate with kernel32 (Windows)')
			from ctypes import windll
			library_loader = windll

		class PowerClass(Structure):
			_fields_ = [
				('ACLineStatus', c_byte),
				('BatteryFlag', c_byte),
				('BatteryLifePercent', c_byte),
				('Reserved1', c_byte),
				('BatteryLifeTime', c_ulong),
				('BatteryFullLifeTime', c_ulong)
			]

		def _get_battery_status(pl):
			powerclass = PowerClass()
			result = library_loader.kernel32.GetSystemPowerStatus(byref(powerclass))
			# http://msdn.microsoft.com/en-us/library/windows/desktop/aa372693(v=vs.85).aspx
			if result:
				return None
			return powerclass.BatteryLifePercent, powerclass.ACLineStatus == 1

		if _get_battery_status() is None:
			pl.debug('Not using GetSystemPowerStatus because it failed')
		else:
			pl.debug('Using GetSystemPowerStatus')

		return _get_battery_status

	raise NotImplementedError


def _get_battery_status(pl):
	global _get_battery_status

	def _failing_get_status(pl):
		raise NotImplementedError

	try:
		_get_battery_status = _fetch_battery_info(pl)
	except NotImplementedError:
		_get_battery_status = _failing_get_status
	except Exception as e:
		pl.exception('Exception while obtaining battery status: {0}', str(e))
		_get_battery_status = _failing_get_status
	return _get_battery_status(pl)


def battery(pl, format='{ac_state} {capacity:3.0%}', steps=5, gamify=False, full_heart='O', empty_heart='O', online='C', offline=' '):
	'''Return battery charge status.

	:param str format:
		Percent format in case gamify is False. Format arguments: ``ac_state`` 
		which is equal to either ``online`` or ``offline`` string arguments and 
		``capacity`` which is equal to current battery capacity in interval [0, 
		100].
	:param int steps:
		Number of discrete steps to show between 0% and 100% capacity if gamify
		is True.
	:param bool gamify:
		Measure in hearts (♥) instead of percentages. For full hearts 
		``battery_full`` highlighting group is preferred, for empty hearts there 
		is ``battery_empty``. ``battery_online`` or ``battery_offline`` group 
		will be used for leading segment containing ``online`` or ``offline`` 
		argument contents.
	:param str full_heart:
		Heart displayed for “full” part of battery.
	:param str empty_heart:
		Heart displayed for “used” part of battery. It is also displayed using
		another gradient level and highlighting group, so it is OK for it to be 
		the same as full_heart as long as necessary highlighting groups are 
		defined.
	:param str online:
		Symbol used if computer is connected to a power supply.
	:param str offline:
		Symbol used if computer is not connected to a power supply.

	``battery_gradient`` and ``battery`` groups are used in any case, first is 
	preferred.

	Highlight groups used: ``battery_full`` or ``battery_gradient`` (gradient) or ``battery``, ``battery_empty`` or ``battery_gradient`` (gradient) or ``battery``, ``battery_online`` or ``battery_ac_state`` or ``battery_gradient`` (gradient) or ``battery``, ``battery_offline`` or ``battery_ac_state`` or ``battery_gradient`` (gradient) or ``battery``.
	'''
	try:
		capacity, ac_powered = _get_battery_status(pl)
	except NotImplementedError:
		pl.info('Unable to get battery status.')
		return None

	ret = []
	if gamify:
		denom = int(steps)
		numer = int(denom * capacity / 100)
		ret.append({
			'contents': online if ac_powered else offline,
			'draw_inner_divider': False,
			'highlight_groups': ['battery_online' if ac_powered else 'battery_offline', 'battery_ac_state', 'battery_gradient', 'battery'],
			'gradient_level': 0,
		})
		ret.append({
			'contents': full_heart * numer,
			'draw_inner_divider': False,
			'highlight_groups': ['battery_full', 'battery_gradient', 'battery'],
			# Using zero as “nothing to worry about”: it is least alert color.
			'gradient_level': 0,
		})
		ret.append({
			'contents': empty_heart * (denom - numer),
			'draw_inner_divider': False,
			'highlight_groups': ['battery_empty', 'battery_gradient', 'battery'],
			# Using a hundred as it is most alert color.
			'gradient_level': 100,
		})
	else:
		ret.append({
			'contents': format.format(ac_state=(online if ac_powered else offline), capacity=(capacity / 100.0)),
			'highlight_groups': ['battery_gradient', 'battery'],
			# Gradients are “least alert – most alert” by default, capacity has 
			# the opposite semantics.
			'gradient_level': 100 - capacity,
		})
	return ret