Home | History | Annotate | Download | only in android
      1 # Copyright 2015 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """Provides a variety of device interactions with power.
      6 """
      7 # pylint: disable=unused-argument
      8 
      9 import collections
     10 import contextlib
     11 import csv
     12 import logging
     13 
     14 from devil.android import decorators
     15 from devil.android import device_errors
     16 from devil.android import device_utils
     17 from devil.android.sdk import version_codes
     18 from devil.utils import timeout_retry
     19 
     20 _DEFAULT_TIMEOUT = 30
     21 _DEFAULT_RETRIES = 3
     22 
     23 
     24 _DEVICE_PROFILES = [
     25   {
     26     'name': 'Nexus 4',
     27     'witness_file': '/sys/module/pm8921_charger/parameters/disabled',
     28     'enable_command': (
     29         'echo 0 > /sys/module/pm8921_charger/parameters/disabled && '
     30         'dumpsys battery reset'),
     31     'disable_command': (
     32         'echo 1 > /sys/module/pm8921_charger/parameters/disabled && '
     33         'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
     34     'charge_counter': None,
     35     'voltage': None,
     36     'current': None,
     37   },
     38   {
     39     'name': 'Nexus 5',
     40     # Nexus 5
     41     # Setting the HIZ bit of the bq24192 causes the charger to actually ignore
     42     # energy coming from USB. Setting the power_supply offline just updates the
     43     # Android system to reflect that.
     44     'witness_file': '/sys/kernel/debug/bq24192/INPUT_SRC_CONT',
     45     'enable_command': (
     46         'echo 0x4A > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && '
     47         'chmod 644 /sys/class/power_supply/usb/online && '
     48         'echo 1 > /sys/class/power_supply/usb/online && '
     49         'dumpsys battery reset'),
     50     'disable_command': (
     51         'echo 0xCA > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && '
     52         'chmod 644 /sys/class/power_supply/usb/online && '
     53         'echo 0 > /sys/class/power_supply/usb/online && '
     54         'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
     55     'charge_counter': None,
     56     'voltage': None,
     57     'current': None,
     58   },
     59   {
     60     'name': 'Nexus 6',
     61     'witness_file': None,
     62     'enable_command': (
     63         'echo 1 > /sys/class/power_supply/battery/charging_enabled && '
     64         'dumpsys battery reset'),
     65     'disable_command': (
     66         'echo 0 > /sys/class/power_supply/battery/charging_enabled && '
     67         'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
     68     'charge_counter': (
     69         '/sys/class/power_supply/max170xx_battery/charge_counter_ext'),
     70     'voltage': '/sys/class/power_supply/max170xx_battery/voltage_now',
     71     'current': '/sys/class/power_supply/max170xx_battery/current_now',
     72   },
     73   {
     74     'name': 'Nexus 9',
     75     'witness_file': None,
     76     'enable_command': (
     77         'echo Disconnected > '
     78         '/sys/bus/i2c/drivers/bq2419x/0-006b/input_cable_state && '
     79         'dumpsys battery reset'),
     80     'disable_command': (
     81         'echo Connected > '
     82         '/sys/bus/i2c/drivers/bq2419x/0-006b/input_cable_state && '
     83         'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
     84     'charge_counter': '/sys/class/power_supply/battery/charge_counter_ext',
     85     'voltage': '/sys/class/power_supply/battery/voltage_now',
     86     'current': '/sys/class/power_supply/battery/current_now',
     87   },
     88   {
     89     'name': 'Nexus 10',
     90     'witness_file': None,
     91     'enable_command': None,
     92     'disable_command': None,
     93     'charge_counter': None,
     94     'voltage': '/sys/class/power_supply/ds2784-fuelgauge/voltage_now',
     95     'current': '/sys/class/power_supply/ds2784-fuelgauge/current_now',
     96 
     97   },
     98   {
     99     'name': 'Nexus 5X',
    100     'witness_file': None,
    101     'enable_command': (
    102         'echo 1 > /sys/class/power_supply/battery/charging_enabled && '
    103         'dumpsys battery reset'),
    104     'disable_command': (
    105         'echo 0 > /sys/class/power_supply/battery/charging_enabled && '
    106         'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
    107     'charge_counter': None,
    108     'voltage': None,
    109     'current': None,
    110   },
    111 ]
    112 
    113 # The list of useful dumpsys columns.
    114 # Index of the column containing the format version.
    115 _DUMP_VERSION_INDEX = 0
    116 # Index of the column containing the type of the row.
    117 _ROW_TYPE_INDEX = 3
    118 # Index of the column containing the uid.
    119 _PACKAGE_UID_INDEX = 4
    120 # Index of the column containing the application package.
    121 _PACKAGE_NAME_INDEX = 5
    122 # The column containing the uid of the power data.
    123 _PWI_UID_INDEX = 1
    124 # The column containing the type of consumption. Only consumption since last
    125 # charge are of interest here.
    126 _PWI_AGGREGATION_INDEX = 2
    127 _PWS_AGGREGATION_INDEX = _PWI_AGGREGATION_INDEX
    128 # The column containing the amount of power used, in mah.
    129 _PWI_POWER_CONSUMPTION_INDEX = 5
    130 _PWS_POWER_CONSUMPTION_INDEX = _PWI_POWER_CONSUMPTION_INDEX
    131 
    132 _MAX_CHARGE_ERROR = 20
    133 
    134 
    135 class BatteryUtils(object):
    136 
    137   def __init__(self, device, default_timeout=_DEFAULT_TIMEOUT,
    138                default_retries=_DEFAULT_RETRIES):
    139     """BatteryUtils constructor.
    140 
    141       Args:
    142         device: A DeviceUtils instance.
    143         default_timeout: An integer containing the default number of seconds to
    144                          wait for an operation to complete if no explicit value
    145                          is provided.
    146         default_retries: An integer containing the default number or times an
    147                          operation should be retried on failure if no explicit
    148                          value is provided.
    149       Raises:
    150         TypeError: If it is not passed a DeviceUtils instance.
    151     """
    152     if not isinstance(device, device_utils.DeviceUtils):
    153       raise TypeError('Must be initialized with DeviceUtils object.')
    154     self._device = device
    155     self._cache = device.GetClientCache(self.__class__.__name__)
    156     self._default_timeout = default_timeout
    157     self._default_retries = default_retries
    158 
    159   @decorators.WithTimeoutAndRetriesFromInstance()
    160   def SupportsFuelGauge(self, timeout=None, retries=None):
    161     """Detect if fuel gauge chip is present.
    162 
    163     Args:
    164       timeout: timeout in seconds
    165       retries: number of retries
    166 
    167     Returns:
    168       True if known fuel gauge files are present.
    169       False otherwise.
    170     """
    171     self._DiscoverDeviceProfile()
    172     return (self._cache['profile']['enable_command'] != None
    173         and self._cache['profile']['charge_counter'] != None)
    174 
    175   @decorators.WithTimeoutAndRetriesFromInstance()
    176   def GetFuelGaugeChargeCounter(self, timeout=None, retries=None):
    177     """Get value of charge_counter on fuel gauge chip.
    178 
    179     Device must have charging disabled for this, not just battery updates
    180     disabled. The only device that this currently works with is the nexus 5.
    181 
    182     Args:
    183       timeout: timeout in seconds
    184       retries: number of retries
    185 
    186     Returns:
    187       value of charge_counter for fuel gauge chip in units of nAh.
    188 
    189     Raises:
    190       device_errors.CommandFailedError: If fuel gauge chip not found.
    191     """
    192     if self.SupportsFuelGauge():
    193       return int(self._device.ReadFile(
    194           self._cache['profile']['charge_counter']))
    195     raise device_errors.CommandFailedError(
    196         'Unable to find fuel gauge.')
    197 
    198   @decorators.WithTimeoutAndRetriesFromInstance()
    199   def GetNetworkData(self, package, timeout=None, retries=None):
    200     """Get network data for specific package.
    201 
    202     Args:
    203       package: package name you want network data for.
    204       timeout: timeout in seconds
    205       retries: number of retries
    206 
    207     Returns:
    208       Tuple of (sent_data, recieved_data)
    209       None if no network data found
    210     """
    211     # If device_utils clears cache, cache['uids'] doesn't exist
    212     if 'uids' not in self._cache:
    213       self._cache['uids'] = {}
    214     if package not in self._cache['uids']:
    215       self.GetPowerData()
    216       if package not in self._cache['uids']:
    217         logging.warning('No UID found for %s. Can\'t get network data.',
    218                         package)
    219         return None
    220 
    221     network_data_path = '/proc/uid_stat/%s/' % self._cache['uids'][package]
    222     try:
    223       send_data = int(self._device.ReadFile(network_data_path + 'tcp_snd'))
    224     # If ReadFile throws exception, it means no network data usage file for
    225     # package has been recorded. Return 0 sent and 0 received.
    226     except device_errors.AdbShellCommandFailedError:
    227       logging.warning('No sent data found for package %s', package)
    228       send_data = 0
    229     try:
    230       recv_data = int(self._device.ReadFile(network_data_path + 'tcp_rcv'))
    231     except device_errors.AdbShellCommandFailedError:
    232       logging.warning('No received data found for package %s', package)
    233       recv_data = 0
    234     return (send_data, recv_data)
    235 
    236   @decorators.WithTimeoutAndRetriesFromInstance()
    237   def GetPowerData(self, timeout=None, retries=None):
    238     """Get power data for device.
    239 
    240     Args:
    241       timeout: timeout in seconds
    242       retries: number of retries
    243 
    244     Returns:
    245       Dict containing system power, and a per-package power dict keyed on
    246       package names.
    247       {
    248         'system_total': 23.1,
    249         'per_package' : {
    250           package_name: {
    251             'uid': uid,
    252             'data': [1,2,3]
    253           },
    254         }
    255       }
    256     """
    257     if 'uids' not in self._cache:
    258       self._cache['uids'] = {}
    259     dumpsys_output = self._device.RunShellCommand(
    260         ['dumpsys', 'batterystats', '-c'],
    261         check_return=True, large_output=True)
    262     csvreader = csv.reader(dumpsys_output)
    263     pwi_entries = collections.defaultdict(list)
    264     system_total = None
    265     for entry in csvreader:
    266       if entry[_DUMP_VERSION_INDEX] not in ['8', '9']:
    267         # Wrong dumpsys version.
    268         raise device_errors.DeviceVersionError(
    269             'Dumpsys version must be 8 or 9. "%s" found.'
    270             % entry[_DUMP_VERSION_INDEX])
    271       if _ROW_TYPE_INDEX < len(entry) and entry[_ROW_TYPE_INDEX] == 'uid':
    272         current_package = entry[_PACKAGE_NAME_INDEX]
    273         if (self._cache['uids'].get(current_package)
    274             and self._cache['uids'].get(current_package)
    275             != entry[_PACKAGE_UID_INDEX]):
    276           raise device_errors.CommandFailedError(
    277               'Package %s found multiple times with different UIDs %s and %s'
    278                % (current_package, self._cache['uids'][current_package],
    279                entry[_PACKAGE_UID_INDEX]))
    280         self._cache['uids'][current_package] = entry[_PACKAGE_UID_INDEX]
    281       elif (_PWI_POWER_CONSUMPTION_INDEX < len(entry)
    282           and entry[_ROW_TYPE_INDEX] == 'pwi'
    283           and entry[_PWI_AGGREGATION_INDEX] == 'l'):
    284         pwi_entries[entry[_PWI_UID_INDEX]].append(
    285             float(entry[_PWI_POWER_CONSUMPTION_INDEX]))
    286       elif (_PWS_POWER_CONSUMPTION_INDEX < len(entry)
    287           and entry[_ROW_TYPE_INDEX] == 'pws'
    288           and entry[_PWS_AGGREGATION_INDEX] == 'l'):
    289         # This entry should only appear once.
    290         assert system_total is None
    291         system_total = float(entry[_PWS_POWER_CONSUMPTION_INDEX])
    292 
    293     per_package = {p: {'uid': uid, 'data': pwi_entries[uid]}
    294                    for p, uid in self._cache['uids'].iteritems()}
    295     return {'system_total': system_total, 'per_package': per_package}
    296 
    297   @decorators.WithTimeoutAndRetriesFromInstance()
    298   def GetBatteryInfo(self, timeout=None, retries=None):
    299     """Gets battery info for the device.
    300 
    301     Args:
    302       timeout: timeout in seconds
    303       retries: number of retries
    304     Returns:
    305       A dict containing various battery information as reported by dumpsys
    306       battery.
    307     """
    308     result = {}
    309     # Skip the first line, which is just a header.
    310     for line in self._device.RunShellCommand(
    311         ['dumpsys', 'battery'], check_return=True)[1:]:
    312       # If usb charging has been disabled, an extra line of header exists.
    313       if 'UPDATES STOPPED' in line:
    314         logging.warning('Dumpsys battery not receiving updates. '
    315                         'Run dumpsys battery reset if this is in error.')
    316       elif ':' not in line:
    317         logging.warning('Unknown line found in dumpsys battery: "%s"', line)
    318       else:
    319         k, v = line.split(':', 1)
    320         result[k.strip()] = v.strip()
    321     return result
    322 
    323   @decorators.WithTimeoutAndRetriesFromInstance()
    324   def GetCharging(self, timeout=None, retries=None):
    325     """Gets the charging state of the device.
    326 
    327     Args:
    328       timeout: timeout in seconds
    329       retries: number of retries
    330     Returns:
    331       True if the device is charging, false otherwise.
    332     """
    333     battery_info = self.GetBatteryInfo()
    334     for k in ('AC powered', 'USB powered', 'Wireless powered'):
    335       if (k in battery_info and
    336           battery_info[k].lower() in ('true', '1', 'yes')):
    337         return True
    338     return False
    339 
    340   # TODO(rnephew): Make private when all use cases can use the context manager.
    341   @decorators.WithTimeoutAndRetriesFromInstance()
    342   def DisableBatteryUpdates(self, timeout=None, retries=None):
    343     """Resets battery data and makes device appear like it is not
    344     charging so that it will collect power data since last charge.
    345 
    346     Args:
    347       timeout: timeout in seconds
    348       retries: number of retries
    349 
    350     Raises:
    351       device_errors.CommandFailedError: When resetting batterystats fails to
    352         reset power values.
    353       device_errors.DeviceVersionError: If device is not L or higher.
    354     """
    355     def battery_updates_disabled():
    356       return self.GetCharging() is False
    357 
    358     self._ClearPowerData()
    359     self._device.RunShellCommand(['dumpsys', 'battery', 'set', 'ac', '0'],
    360                                  check_return=True)
    361     self._device.RunShellCommand(['dumpsys', 'battery', 'set', 'usb', '0'],
    362                                  check_return=True)
    363     timeout_retry.WaitFor(battery_updates_disabled, wait_period=1)
    364 
    365   # TODO(rnephew): Make private when all use cases can use the context manager.
    366   @decorators.WithTimeoutAndRetriesFromInstance()
    367   def EnableBatteryUpdates(self, timeout=None, retries=None):
    368     """Restarts device charging so that dumpsys no longer collects power data.
    369 
    370     Args:
    371       timeout: timeout in seconds
    372       retries: number of retries
    373 
    374     Raises:
    375       device_errors.DeviceVersionError: If device is not L or higher.
    376     """
    377     def battery_updates_enabled():
    378       return (self.GetCharging()
    379               or not bool('UPDATES STOPPED' in self._device.RunShellCommand(
    380                   ['dumpsys', 'battery'], check_return=True)))
    381 
    382     self._device.RunShellCommand(['dumpsys', 'battery', 'reset'],
    383                                  check_return=True)
    384     timeout_retry.WaitFor(battery_updates_enabled, wait_period=1)
    385 
    386   @contextlib.contextmanager
    387   def BatteryMeasurement(self, timeout=None, retries=None):
    388     """Context manager that enables battery data collection. It makes
    389     the device appear to stop charging so that dumpsys will start collecting
    390     power data since last charge. Once the with block is exited, charging is
    391     resumed and power data since last charge is no longer collected.
    392 
    393     Only for devices L and higher.
    394 
    395     Example usage:
    396       with BatteryMeasurement():
    397         browser_actions()
    398         get_power_data() # report usage within this block
    399       after_measurements() # Anything that runs after power
    400                            # measurements are collected
    401 
    402     Args:
    403       timeout: timeout in seconds
    404       retries: number of retries
    405 
    406     Raises:
    407       device_errors.DeviceVersionError: If device is not L or higher.
    408     """
    409     if self._device.build_version_sdk < version_codes.LOLLIPOP:
    410       raise device_errors.DeviceVersionError('Device must be L or higher.')
    411     try:
    412       self.DisableBatteryUpdates(timeout=timeout, retries=retries)
    413       yield
    414     finally:
    415       self.EnableBatteryUpdates(timeout=timeout, retries=retries)
    416 
    417   def _DischargeDevice(self, percent, wait_period=120):
    418     """Disables charging and waits for device to discharge given amount
    419 
    420     Args:
    421       percent: level of charge to discharge.
    422 
    423     Raises:
    424       ValueError: If percent is not between 1 and 99.
    425     """
    426     battery_level = int(self.GetBatteryInfo().get('level'))
    427     if not 0 < percent < 100:
    428       raise ValueError('Discharge amount(%s) must be between 1 and 99'
    429                        % percent)
    430     if battery_level is None:
    431       logging.warning('Unable to find current battery level. Cannot discharge.')
    432       return
    433     # Do not discharge if it would make battery level too low.
    434     if percent >= battery_level - 10:
    435       logging.warning('Battery is too low or discharge amount requested is too '
    436                       'high. Cannot discharge phone %s percent.', percent)
    437       return
    438 
    439     self._HardwareSetCharging(False)
    440 
    441     def device_discharged():
    442       self._HardwareSetCharging(True)
    443       current_level = int(self.GetBatteryInfo().get('level'))
    444       logging.info('current battery level: %s', current_level)
    445       if battery_level - current_level >= percent:
    446         return True
    447       self._HardwareSetCharging(False)
    448       return False
    449 
    450     timeout_retry.WaitFor(device_discharged, wait_period=wait_period)
    451 
    452   def ChargeDeviceToLevel(self, level, wait_period=60):
    453     """Enables charging and waits for device to be charged to given level.
    454 
    455     Args:
    456       level: level of charge to wait for.
    457       wait_period: time in seconds to wait between checking.
    458     Raises:
    459       device_errors.DeviceChargingError: If error while charging is detected.
    460     """
    461     self.SetCharging(True)
    462     charge_status = {
    463         'charge_failure_count': 0,
    464         'last_charge_value': 0
    465     }
    466     def device_charged():
    467       battery_level = self.GetBatteryInfo().get('level')
    468       if battery_level is None:
    469         logging.warning('Unable to find current battery level.')
    470         battery_level = 100
    471       else:
    472         logging.info('current battery level: %s', battery_level)
    473         battery_level = int(battery_level)
    474 
    475       # Use > so that it will not reset if charge is going down.
    476       if battery_level > charge_status['last_charge_value']:
    477         charge_status['last_charge_value'] = battery_level
    478         charge_status['charge_failure_count'] = 0
    479       else:
    480         charge_status['charge_failure_count'] += 1
    481 
    482       if (not battery_level >= level
    483           and charge_status['charge_failure_count'] >= _MAX_CHARGE_ERROR):
    484         raise device_errors.DeviceChargingError(
    485             'Device not charging properly. Current level:%s Previous level:%s'
    486              % (battery_level, charge_status['last_charge_value']))
    487       return battery_level >= level
    488 
    489     timeout_retry.WaitFor(device_charged, wait_period=wait_period)
    490 
    491   def LetBatteryCoolToTemperature(self, target_temp, wait_period=180):
    492     """Lets device sit to give battery time to cool down
    493     Args:
    494       temp: maximum temperature to allow in tenths of degrees c.
    495       wait_period: time in seconds to wait between checking.
    496     """
    497     def cool_device():
    498       temp = self.GetBatteryInfo().get('temperature')
    499       if temp is None:
    500         logging.warning('Unable to find current battery temperature.')
    501         temp = 0
    502       else:
    503         logging.info('Current battery temperature: %s', temp)
    504       if int(temp) <= target_temp:
    505         return True
    506       else:
    507         if self._cache['profile']['name'] == 'Nexus 5':
    508           self._DischargeDevice(1)
    509         return False
    510 
    511     self._DiscoverDeviceProfile()
    512     self.EnableBatteryUpdates()
    513     logging.info('Waiting for the device to cool down to %s (0.1 C)',
    514                  target_temp)
    515     timeout_retry.WaitFor(cool_device, wait_period=wait_period)
    516 
    517   @decorators.WithTimeoutAndRetriesFromInstance()
    518   def SetCharging(self, enabled, timeout=None, retries=None):
    519     """Enables or disables charging on the device.
    520 
    521     Args:
    522       enabled: A boolean indicating whether charging should be enabled or
    523         disabled.
    524       timeout: timeout in seconds
    525       retries: number of retries
    526     """
    527     if self.GetCharging() == enabled:
    528       logging.warning('Device charging already in expected state: %s', enabled)
    529       return
    530 
    531     self._DiscoverDeviceProfile()
    532     if enabled:
    533       if self._cache['profile']['enable_command']:
    534         self._HardwareSetCharging(enabled)
    535       else:
    536         logging.info('Unable to enable charging via hardware. '
    537                      'Falling back to software enabling.')
    538         self.EnableBatteryUpdates()
    539     else:
    540       if self._cache['profile']['enable_command']:
    541         self._ClearPowerData()
    542         self._HardwareSetCharging(enabled)
    543       else:
    544         logging.info('Unable to disable charging via hardware. '
    545                      'Falling back to software disabling.')
    546         self.DisableBatteryUpdates()
    547 
    548   def _HardwareSetCharging(self, enabled, timeout=None, retries=None):
    549     """Enables or disables charging on the device.
    550 
    551     Args:
    552       enabled: A boolean indicating whether charging should be enabled or
    553         disabled.
    554       timeout: timeout in seconds
    555       retries: number of retries
    556 
    557     Raises:
    558       device_errors.CommandFailedError: If method of disabling charging cannot
    559         be determined.
    560     """
    561     self._DiscoverDeviceProfile()
    562     if not self._cache['profile']['enable_command']:
    563       raise device_errors.CommandFailedError(
    564           'Unable to find charging commands.')
    565 
    566     command = (self._cache['profile']['enable_command'] if enabled
    567                else self._cache['profile']['disable_command'])
    568 
    569     def verify_charging():
    570       return self.GetCharging() == enabled
    571 
    572     self._device.RunShellCommand(
    573         command, check_return=True, as_root=True, large_output=True)
    574     timeout_retry.WaitFor(verify_charging, wait_period=1)
    575 
    576   @contextlib.contextmanager
    577   def PowerMeasurement(self, timeout=None, retries=None):
    578     """Context manager that enables battery power collection.
    579 
    580     Once the with block is exited, charging is resumed. Will attempt to disable
    581     charging at the hardware level, and if that fails will fall back to software
    582     disabling of battery updates.
    583 
    584     Only for devices L and higher.
    585 
    586     Example usage:
    587       with PowerMeasurement():
    588         browser_actions()
    589         get_power_data() # report usage within this block
    590       after_measurements() # Anything that runs after power
    591                            # measurements are collected
    592 
    593     Args:
    594       timeout: timeout in seconds
    595       retries: number of retries
    596     """
    597     try:
    598       self.SetCharging(False, timeout=timeout, retries=retries)
    599       yield
    600     finally:
    601       self.SetCharging(True, timeout=timeout, retries=retries)
    602 
    603   def _ClearPowerData(self):
    604     """Resets battery data and makes device appear like it is not
    605     charging so that it will collect power data since last charge.
    606 
    607     Returns:
    608       True if power data cleared.
    609       False if power data clearing is not supported (pre-L)
    610 
    611     Raises:
    612       device_errors.DeviceVersionError: If power clearing is supported,
    613         but fails.
    614     """
    615     if self._device.build_version_sdk < version_codes.LOLLIPOP:
    616       logging.warning('Dumpsys power data only available on 5.0 and above. '
    617                       'Cannot clear power data.')
    618       return False
    619 
    620     self._device.RunShellCommand(
    621         ['dumpsys', 'battery', 'set', 'usb', '1'], check_return=True)
    622     self._device.RunShellCommand(
    623         ['dumpsys', 'battery', 'set', 'ac', '1'], check_return=True)
    624 
    625     def test_if_clear():
    626       self._device.RunShellCommand(
    627           ['dumpsys', 'batterystats', '--reset'], check_return=True)
    628       battery_data = self._device.RunShellCommand(
    629           ['dumpsys', 'batterystats', '--charged', '-c'],
    630           check_return=True, large_output=True)
    631       for line in battery_data:
    632         l = line.split(',')
    633         if (len(l) > _PWI_POWER_CONSUMPTION_INDEX
    634             and l[_ROW_TYPE_INDEX] == 'pwi'
    635             and float(l[_PWI_POWER_CONSUMPTION_INDEX]) != 0.0):
    636           return False
    637       return True
    638 
    639     try:
    640       timeout_retry.WaitFor(test_if_clear, wait_period=1)
    641       return True
    642     finally:
    643       self._device.RunShellCommand(
    644           ['dumpsys', 'battery', 'reset'], check_return=True)
    645 
    646   def _DiscoverDeviceProfile(self):
    647     """Checks and caches device information.
    648 
    649     Returns:
    650       True if profile is found, false otherwise.
    651     """
    652 
    653     if 'profile' in self._cache:
    654       return True
    655     for profile in _DEVICE_PROFILES:
    656       if self._device.product_model == profile['name']:
    657         self._cache['profile'] = profile
    658         return True
    659     self._cache['profile'] = {
    660         'name': None,
    661         'witness_file': None,
    662         'enable_command': None,
    663         'disable_command': None,
    664         'charge_counter': None,
    665         'voltage': None,
    666         'current': None,
    667     }
    668     return False
    669