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