blob: 3a9adbdd177480a8e38e210f878cdb610407d094 [file] [log] [blame]
# Copyright 2015 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from __future__ import division
import collections
import logging
import re
import time
from six.moves import xrange
import factory_common # pylint: disable=unused-import
from cros.factory.device import types
from cros.factory.utils import type_utils
from cros.factory.external import numpy
class PowerException(types.DeviceException):
pass
def CreatePower(dut, *mixins, **kargs):
"""Creates an instance of Power class inherited from PowerBase with mixins.
This function is equivalent to
def CreatePower(dut, *mixins, **kargs):
class Power(*mixins, PowerBase):
pass
return Power(dut, **kargs)
except that we actually cannot use *mixins in class declaration.
Args:
dut: A types.DeviceBoard instance.
mixins: One or more mixin classes.
kargs: Key arguments to pass to base constructor.
Returns:
An instance of Power class with given mixins, with `dut` and `kargs` passed
as constructor arguments.
Example:
power = CreatePower(dut, ECToolPowerControlMixin, ECToolPowerInfoMixin)
"""
bases = mixins + (PowerBase,)
power_cls = type('Power', bases, {})
return power_cls(dut, **kargs)
class PowerBase(types.DeviceComponent):
"""Base class for power.
The base class is basically empty, and needs mixin classes to add its
functions.
"""
# Power source types
PowerSource = type_utils.Enum(['BATTERY', 'AC'])
# An enumeration of possible charge states.
# - ``CHARGE``: Charge the device as usual.
# - ``IDLE``: Do not charge the device, even if connected to mains.
# - ``DISCHARGE``: Force the device to discharge.
ChargeState = type_utils.Enum(['CHARGE', 'IDLE', 'DISCHARGE'])
def __init__(self, dut, pd_name=None):
super(PowerBase, self).__init__(dut)
self._pd_name = pd_name
class PowerControlMixinBase(object):
"""Base class for power control mixin."""
def SetChargeState(self, state):
"""Sets the charge state."""
raise NotImplementedError
class DummyPowerControlMixin(PowerControlMixinBase):
"""Power control mixin that does nothing."""
def SetChargeState(self, state):
"""See PowerControlMixinBase.SetChargeState"""
pass
class ECToolPowerControlMixin(PowerControlMixinBase):
"""Power control mixin that uses ectool."""
def SetChargeState(self, state):
"""See PowerControlMixinBase.SetChargeState"""
try:
if state == self.ChargeState.CHARGE:
self._device.CheckCall(['ectool', 'chargecontrol', 'normal'])
elif state == self.ChargeState.IDLE:
self._device.CheckCall(['ectool', 'chargecontrol', 'idle'])
elif state == self.ChargeState.DISCHARGE:
self._device.CheckCall(['ectool', 'chargecontrol', 'discharge'])
else:
raise self.Error('Unknown EC charge state: %s' % state)
except Exception as e:
raise self.Error('Unable to set charge state: %s' % e)
class PowerInfoMixinBase(object):
"""Base class for power info mixin."""
_CHARGE_STATE_MAP = {
'Charging': PowerBase.ChargeState.CHARGE,
'Idle': PowerBase.ChargeState.IDLE,
'Discharging': PowerBase.ChargeState.DISCHARGE
}
def CheckACPresent(self):
"""Check if AC power is present."""
raise NotImplementedError
def GetACType(self):
"""Get AC power type."""
raise NotImplementedError
def CheckBatteryPresent(self):
"""Check if battery is present."""
raise NotImplementedError
def GetCharge(self):
"""Get current charge level in mAh."""
raise NotImplementedError
def GetChargeMedian(self, read_count=10):
"""Read charge level several times and return the median."""
charge_nows = []
for _ in xrange(read_count):
charge_now = self.GetCharge()
if charge_now:
charge_nows.append(charge_now)
time.sleep(0.1)
return numpy.median(charge_nows)
def GetChargeFull(self):
"""Get full charge level in mAh."""
raise NotImplementedError
def GetChargePct(self, get_float=False):
"""Get current charge level in percentage.
Args:
get_float: Returns charge percentage in float.
Returns:
Charge percentage in int/float.
"""
raise NotImplementedError
def GetWearPct(self):
"""Get current battery wear in percentage of new capacity."""
raise NotImplementedError
def GetChargeState(self):
"""Returns the charge state.
Returns:
One of the three states in ChargeState.
"""
raise NotImplementedError
def GetChargerCurrent(self):
"""Gets the amount of current we ask from charger.
Returns:
Interger value in mA.
"""
raise NotImplementedError
def GetBatteryCurrent(self):
"""Gets the amount of current battery is charging/discharging at.
Returns:
Integer value in mA.
"""
raise NotImplementedError
def GetBatteryDesignCapacity(self):
"""Gets battery's design capacity.
Returns:
Battery's design capacity in mAh.
Raises:
DeviceException if battery's design capacity cannot be obtained.
"""
raise NotImplementedError
def GetBatteryVoltage(self):
"""Gets battery's current voltage.
Returns:
Battery's current voltage in mV.
"""
raise NotImplementedError
def GetBatteryCycleCount(self):
"""Gets battery's cycle count."""
raise NotImplementedError
def GetBatteryManufacturer(self):
"""Gets battery's manufacturer."""
raise NotImplementedError
def GetInfoDict(self):
"""Returns a dict containing information about the battery.
TODO(kitching): Determine whether this function is necessary (who uses it?).
"""
_SysfsBatteryAttributes = [
('current_now', self.GetBatteryCurrent),
('present', self.CheckBatteryPresent),
('status', self.GetChargeState),
('voltage_now', self.GetBatteryVoltage),
('charge_full', self.GetChargeFull),
('charge_full_design', self.GetBatteryDesignCapacity),
('charge_now', self.GetCharge),
('fraction_full', lambda: self.GetChargePct(True) / 100),
]
result = {}
for k, getter in _SysfsBatteryAttributes:
try:
result[k] = getter()
except Exception as e:
exc_str = '%s: %s' % (e.__class__.__name__, e)
logging.error('battery attribute %s is unavailable: %s', k, exc_str)
return result
class SysfsPowerInfoMixin(PowerInfoMixinBase):
"""Power info mixin that uses sysfs files."""
_sys = '/sys'
# Regular expression for parsing charger current.
EC_CHARGER_CURRENT_RE = re.compile(r'^chg_current = (\d+)mA', re.MULTILINE)
def ReadOneLine(self, file_path):
"""Reads one stripped line from given file on DUT.
Args:
file_path: The file path on DUT.
Returns:
String for the first line of file contents.
"""
# splitlines() does not work on empty string so we have to check.
contents = self._device.ReadSpecialFile(file_path)
if contents:
return contents.splitlines()[0].strip()
return ''
def FindPowerPath(self, power_source):
"""Find battery path in sysfs.
Note few attributes, especially 'online', is usually implemented as cached
value and only updated if we read some dynamic values (for example
voltage_now) or a host event from EC is discovered. This implies if host
events are broken, many values won't be updated - and this is critical for
FindPowerPath.
For devices in early stage, you probably want to extend FindPowerPath by
reading voltage_now from all power_supply entries.
"""
def GetValue(path, sub_path):
full_path = self._device.path.join(path, sub_path)
if not self._device.path.exists(full_path):
return None
return self.ReadOneLine(full_path)
all_power_supplies = self._device.Glob(
self._device.path.join(self._sys, 'class/power_supply/*'))
if power_source == self.PowerSource.BATTERY:
# Some HID peripherals, for example Stylus, may has its own battery and
# appear in power_supply as well, with scope='Device'; and we do want to
# skip them.
power_supplies = [
p for p in all_power_supplies if GetValue(p, 'type') == 'Battery' and
GetValue(p, 'scope') != 'Device'
]
else:
power_supplies = [
p for p in all_power_supplies if GetValue(p, 'type') != 'Battery'
]
if power_supplies:
# Systems with multiple USB-C ports may have multiple power sources.
# Since the end goal is to determine if the system is powered, let's
# just return the first powered AC path if there's any; otherwise
# return the first in the list.
for p in power_supplies:
if GetValue(p, 'online') == '1':
return p
return power_supplies[0]
raise PowerException('Cannot find %s' % power_source)
def CheckACPresent(self):
"""See PowerInfoMixinBase.CheckACPresent"""
try:
p = self.FindPowerPath(self.PowerSource.AC)
return self.ReadOneLine(self._device.path.join(p, 'online')) == '1'
except (PowerException, IOError):
return False
def GetACType(self):
"""See PowerInfoMixinBase.GetACType"""
try:
p = self.FindPowerPath(self.PowerSource.AC)
return self.ReadOneLine(self._device.path.join(p, 'type'))
except (PowerException, IOError):
return 'Unknown'
@types.DeviceProperty
def _battery_path(self):
"""Get battery path.
Use cached value if available.
Returns:
Battery path if available, None otherwise.
"""
try:
return self.FindPowerPath(self.PowerSource.BATTERY)
except PowerException:
return None
def CheckBatteryPresent(self):
"""See PowerInfoMixinBase.CheckBatteryPresent"""
return bool(self._battery_path)
def GetBatteryAttribute(self, attribute_name):
"""Get a battery attribute.
Args:
attribute_name: The name of attribute in sysfs.
Returns:
Content of the attribute in str.
"""
try:
return self.ReadOneLine(
self._device.path.join(self._battery_path, attribute_name))
except IOError:
# Battery driver is not fully initialized
return None
def GetCharge(self):
"""See PowerInfoMixinBase.GetCharge"""
charge_now = self.GetBatteryAttribute('charge_now')
if charge_now:
return int(charge_now) // 1000
else:
return None
def GetChargeFull(self):
"""See PowerInfoMixinBase.GetChargeFull"""
charge_full = self.GetBatteryAttribute('charge_full')
if charge_full:
return int(charge_full) // 1000
else:
return None
def GetChargePct(self, get_float=False):
"""See PowerInfoMixinBase.GetChargePct"""
now = self.GetBatteryAttribute('charge_now')
full = self.GetBatteryAttribute('charge_full')
if now is None or full is None:
now = self.GetBatteryAttribute('energy_now')
full = self.GetBatteryAttribute('energy_full')
if now is None or full is None:
return None
if float(full) <= 0:
return None # Something wrong with the battery
charge_pct = float(now) * 100 / float(full)
if get_float:
return charge_pct
else:
return round(charge_pct)
def GetWearPct(self):
"""See PowerInfoMixinBase.GetWearPct"""
capacity = self.GetBatteryAttribute('charge_full')
design_capacity = self.GetBatteryAttribute('charge_full_design')
if capacity is None or design_capacity is None:
# No charge values, check for energy-reporting batteries
capacity = self.GetBatteryAttribute('energy_full')
design_capacity = self.GetBatteryAttribute('energy_full_design')
if capacity is None or design_capacity is None:
# Battery driver is not fully initialized
return None
if float(design_capacity) <= 0:
return None # Something wrong with the battery
return 100 - (round(capacity * 100 / float(design_capacity)))
def GetChargeState(self):
"""See PowerInfoMixinBase.GetChargeState"""
return self._CHARGE_STATE_MAP[self.GetBatteryAttribute('status')]
def GetChargerCurrent(self):
"""See PowerInfoMixinBase.GetChargerCurrent
TODO(chenghan): Currently cros-usb-pd-charger does not provide 'current_now'
file in sysfs (crbug/807753), so we use ectool to get this
information. Change this function to use 'current_now' when
the issue is fixed.
"""
re_object = self.EC_CHARGER_CURRENT_RE.findall(
self._device.CheckOutput(['ectool', 'chargestate', 'show']))
if re_object:
return int(re_object[0])
else:
raise self.Error('Cannot find current in ectool chargestate show')
def GetBatteryCurrent(self):
"""See PowerInfoMixinBase.GetBatteryCurrent"""
charging = (self.GetBatteryAttribute('status') == 'Charging')
current = self.GetBatteryAttribute('current_now')
if current is None:
raise self.Error('Cannot find %s/current_now' % self._battery_path)
current_ma = abs(int(current)) // 1000
return current_ma if charging else -current_ma
def GetBatteryDesignCapacity(self):
"""See PowerInfoMixinBase.GetBatteryDesignCapacity"""
design_capacity = self.GetBatteryAttribute('charge_full_design')
if design_capacity is None:
raise self.Error('Design capacity not found.')
try:
return int(design_capacity) // 1000
except Exception as e:
raise self.Error('Unable to get battery design capacity: %s' % e)
def GetBatteryVoltage(self):
"""See PowerInfoMixinBase.GetBatteryVoltage"""
voltage = self.GetBatteryAttribute('voltage_now')
return int(voltage) // 1000
def GetBatteryCycleCount(self):
"""See PowerInfoMixinBase.GetBatteryCycleCount"""
return int(self.GetBatteryAttribute('cycle_count'))
def GetBatteryManufacturer(self):
"""See PowerInfoMixinBase.GetBatteryManufacturer"""
return self.GetBatteryAttribute('manufacturer')
class ECToolPowerInfoMixin(PowerInfoMixinBase):
"""Power info mixin that uses ectool."""
# Regular expression for parsing output.
EC_BATTERY_CHARGING_RE = re.compile(r'^\s+Flags\s+.*\s+CHARGING.*$',
re.MULTILINE)
EC_CHARGER_CURRENT_RE = re.compile(r'^chg_current = (\d+)mA', re.MULTILINE)
BATTERY_FLAGS_RE = re.compile(r'Flags\s+(.*)$')
def _GetECToolBatteryFlags(self):
re_object = self.BATTERY_FLAGS_RE.findall(
self._device.CallOutput(['ectool', 'battery']))
if re_object:
return re_object[0].split()
else:
return []
def _GetECToolBatteryAttribute(self, key_name, item_type=str):
re_object = re.findall(r'%s\s+(\S+)' % key_name,
self._device.CallOutput(['ectool', 'battery']))
if re_object:
return item_type(re_object[0])
else:
raise self.Error('Cannot find key "%s" in ectool battery' % key_name)
def CheckACPresent(self):
"""See PowerInfoMixinBase.CheckACPresent"""
return 'AC_PRESENT' in self._GetECToolBatteryFlags()
def GetACType(self):
"""See PowerInfoMixinBase.GetACType.
There is no ectool command to get AC type, so just return 'Unknown'.
"""
return 'Unknown'
def CheckBatteryPresent(self):
"""See PowerInfoMixinBase.CheckBatteryPresent"""
return 'BATT_PRESENT' in self._GetECToolBatteryFlags()
def GetCharge(self):
"""See PowerInfoMixinBase.GetCharge"""
return self._GetECToolBatteryAttribute('Remaining capacity', int)
def GetChargeFull(self):
"""See PowerInfoMixinBase.GetChargeFull"""
return self._GetECToolBatteryAttribute('Last full charge:', int)
def GetChargePct(self, get_float=False):
"""See PowerInfoMixinBase.GetChargePct"""
charge_pct = self.GetCharge() * 100 / self.GetChargeFull()
if get_float:
return charge_pct
else:
return round(charge_pct)
def GetWearPct(self):
"""See PowerInfoMixinBase.GetWearPct"""
capacity = self.GetChargeFull()
design_capacity = self.GetBatteryDesignCapacity()
if design_capacity <= 0:
return None # Something wrong with the battery
return 100 - round(capacity * 100 / design_capacity)
def GetChargeState(self):
"""See PowerInfoMixinBase.GetWearPct"""
if 'CHARGING' in self._GetECToolBatteryFlags():
return self.ChargeState.CHARGE
else:
return self.ChargeState.DISCHARGE
def GetBatteryCurrent(self):
"""See PowerInfoMixinBase.GetBatteryCurrent"""
charging = 'CHARGING' in self._GetECToolBatteryFlags()
current = self._GetECToolBatteryAttribute('Present current', int)
return current if charging else -current
def GetBatteryDesignCapacity(self):
"""See PowerInfoMixinBase.GetBatteryDesignCapacity"""
return self._GetECToolBatteryAttribute('Design capacity:', int)
def GetChargerCurrent(self):
"""See PowerInfoMixinBase.GetChargerCurrent"""
re_object = self.EC_CHARGER_CURRENT_RE.findall(
self._device.CheckOutput(['ectool', 'chargestate', 'show']))
if re_object:
return int(re_object[0])
else:
raise self.Error('Cannot find current in ectool chargestate show')
def GetBatteryVoltage(self):
"""See PowerInfoMixinBase.GetBatteryVoltage"""
return self._GetECToolBatteryAttribute('Present voltage', int)
def GetBatteryCycleCount(self):
"""See PowerInfoMixinBase.GetBatteryCycleCount"""
return self._GetECToolBatteryAttribute('Cycle count', int)
def GetBatteryManufacturer(self):
"""See PowerInfoMixinBase.GetBatteryManufacturer"""
return self._GetECToolBatteryAttribute('OEM name:')
def GetPowerInfo(self):
"""Gets power information.
Returns:
The output of ectool powerinfo, like::
AC Voltage: 5143 mV
System Voltage: 11753 mV
System Current: 1198 mA
System Power: 14080 mW
USB Device Type: 0x20010
USB Current Limit: 2958 mA
It can be further parsed by
:py:func:`cros.factory.utils.string_utils.ParseDict` into a
dict.
Raises:
DeviceException if power information cannot be obtained.
"""
return self._device.CallOutput(['ectool', 'powerinfo'])
def GetUSBPDPowerInfo(self):
"""Gets USB PD power information.
Returns:
The output of ectool usbpdpower, like::
Port 0: Disconnected
Port 1: SNK Charger PD 20714mV / 3000mA, max 20000mV / 3000mA / 60000mW
Port 2: SRC
"""
command = ['ectool', 'usbpdpower']
if self._pd_name:
command.append('--name=' + self._pd_name)
output = self._device.CheckOutput(command)
USBPortInfo = collections.namedtuple(
'USBPortInfo', 'id state voltage current')
ports = []
for line in output.strip().splitlines():
match = re.match(r'Port\s+(\d+):\s+(\w+)', line)
if not match:
raise self.Error('unexpected output: %s' % output)
port_id, port_state = int(match.group(1)), match.group(2)
if port_state not in ['Disconnected', 'SNK', 'SRC']:
raise self.Error('unexpected PD state: %s\noutput="""%s"""' %
(port_state, output))
voltage = None
current = None
if port_state == 'SNK':
match = re.search(r'SNK Charger PD (\d+)mV\s+/\s+(\d+)mA', line)
if not match:
raise self.Error('unexpected output for SNK state: %s' % output)
voltage, current = int(match.group(1)), int(match.group(2))
ports.append(USBPortInfo(port_id, port_state, voltage, current))
return ports
class PowerDaemonPowerInfoMixin(PowerInfoMixinBase):
"""Power info mixin that uses powerd."""
def _GetDumpPowerStatus(self):
return self._device.CallOutput(['dump_power_status'])
def _GetPowerAttribute(self, key_name, item_type=str):
re_object = re.findall(
r'^%s ?(\S*)$' % key_name, self._GetDumpPowerStatus(), re.MULTILINE)
if re_object:
return item_type(re_object[0])
else:
raise self.Error('Cannot find key "%s" in dump_power_status' % key_name)
def CheckACPresent(self):
"""See PowerInfoMixinBase.CheckACPresent"""
return self._GetPowerAttribute('line_power_connected', int) == 1
def GetACType(self):
"""See PowerInfoMixinBase.GetACType"""
return self._GetPowerAttribute('line_power_type')
def CheckBatteryPresent(self):
"""See PowerInfoMixinBase.CheckBatteryPresent"""
return self._GetPowerAttribute('battery_present', int) == 1
def GetCharge(self):
"""See PowerInfoMixinBase.GetCharge"""
return int(self._GetPowerAttribute('battery_charge', float) * 1000)
def GetChargeFull(self):
"""See PowerInfoMixinBase.GetChargeFull"""
return int(self._GetPowerAttribute('battery_charge_full', float) * 1000)
def GetChargePct(self, get_float=False):
"""See PowerInfoMixinBase.GetChargePct"""
charge_pct = self._GetPowerAttribute('battery_percent', float)
if get_float:
return charge_pct
else:
return round(charge_pct)
def GetWearPct(self):
"""See PowerInfoMixinBase.GetWearPct"""
capacity = self.GetChargeFull()
design_capacity = self.GetBatteryDesignCapacity()
if design_capacity <= 0:
return None # Something wrong with the battery
return 100 - round(capacity * 100 / design_capacity)
def GetChargeState(self):
"""See PowerInfoMixinBase.GetChargeState"""
return self._CHARGE_STATE_MAP[self._GetPowerAttribute('battery_status')]
def GetChargerCurrent(self):
"""See PowerInfoMixinBase.GetChargerCurrent
TODO(chenghan): Currently cros-usb-pd-charger does not provide 'current_now'
file in sysfs (crbug/807753), which is read by
`dump_power_status` to get 'line_power_current' field.
Change this function to use 'line_power_current' when the
issue is fixed.
"""
return super(PowerDaemonPowerInfoMixin, self).GetChargerCurrent()
def GetBatteryCurrent(self):
"""See PowerInfoMixinBase.GetBatteryCurrent"""
charging = self.GetChargeState() == self.ChargeState.CHARGE
current = int(self._GetPowerAttribute('battery_current', float) * 1000)
return current if charging else -current
def GetBatteryDesignCapacity(self):
"""See PowerInfoMixinBase.GetBatteryDesignCapacity"""
return int(
self._GetPowerAttribute('battery_charge_full_design', float) * 1000)
def GetBatteryVoltage(self):
"""See PowerInfoMixinBase.GetBatteryVoltage"""
return int(
self._GetPowerAttribute('battery_voltage', float) * 1000)
def GetBatteryCycleCount(self):
"""See PowerInfoMixinBase.GetBatteryCycleCount
TODO(chenghan): Change this function when `dump_power_status` supports
this field.
"""
return super(PowerDaemonPowerInfoMixin, self).GetBatteryCycleCount()
def GetBatteryManufacturer(self):
"""See PowerInfoMixinBase.GetBatteryManufacturer
TODO(chenghan): Change this function when `dump_power_status` supports
this field.
"""
return super(PowerDaemonPowerInfoMixin, self).GetBatteryManufacturer()
class LinuxPower(DummyPowerControlMixin, SysfsPowerInfoMixin, PowerBase):
"""Power with no power control and info from sysfs."""
pass
class ChromeOSPowerLegacy(
ECToolPowerControlMixin, SysfsPowerInfoMixin, PowerBase):
"""Power with ectool power control and info from sysfs."""
pass
class ChromeOSPower(ECToolPowerControlMixin, PowerDaemonPowerInfoMixin,
ECToolPowerInfoMixin, PowerBase):
"""Power with ectool power control and info from powerd.
If powerd does not support the function, fall back to use ectool.
"""
pass
# Some board implementations create their own power class by inheriting from
# power.Power, which is the previous power class with ectool power control
# and sysfs power info. For compatibility we also define Power here.
Power = ChromeOSPowerLegacy