303 lines
10 KiB
Python
303 lines
10 KiB
Python
|
# 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
|