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
9import collections
10import contextlib
11import csv
12import logging
13
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
181  def __init__(self, device, default_timeout=_DEFAULT_TIMEOUT,
182               default_retries=_DEFAULT_RETRIES):
183    """BatteryUtils constructor.
184
185      Args:
186        device: A DeviceUtils instance.
187        default_timeout: An integer containing the default number of seconds to
188                         wait for an operation to complete if no explicit value
189                         is provided.
190        default_retries: An integer containing the default number or times an
191                         operation should be retried on failure if no explicit
192                         value is provided.
193      Raises:
194        TypeError: If it is not passed a DeviceUtils instance.
195    """
196    if not isinstance(device, device_utils.DeviceUtils):
197      raise TypeError('Must be initialized with DeviceUtils object.')
198    self._device = device
199    self._cache = device.GetClientCache(self.__class__.__name__)
200    self._default_timeout = default_timeout
201    self._default_retries = default_retries
202
203  @decorators.WithTimeoutAndRetriesFromInstance()
204  def SupportsFuelGauge(self, timeout=None, retries=None):
205    """Detect if fuel gauge chip is present.
206
207    Args:
208      timeout: timeout in seconds
209      retries: number of retries
210
211    Returns:
212      True if known fuel gauge files are present.
213      False otherwise.
214    """
215    self._DiscoverDeviceProfile()
216    return (self._cache['profile']['enable_command'] != None
217        and self._cache['profile']['charge_counter'] != None)
218
219  @decorators.WithTimeoutAndRetriesFromInstance()
220  def GetFuelGaugeChargeCounter(self, timeout=None, retries=None):
221    """Get value of charge_counter on fuel gauge chip.
222
223    Device must have charging disabled for this, not just battery updates
224    disabled. The only device that this currently works with is the nexus 5.
225
226    Args:
227      timeout: timeout in seconds
228      retries: number of retries
229
230    Returns:
231      value of charge_counter for fuel gauge chip in units of nAh.
232
233    Raises:
234      device_errors.CommandFailedError: If fuel gauge chip not found.
235    """
236    if self.SupportsFuelGauge():
237      return int(self._device.ReadFile(
238          self._cache['profile']['charge_counter']))
239    raise device_errors.CommandFailedError(
240        'Unable to find fuel gauge.')
241
242  @decorators.WithTimeoutAndRetriesFromInstance()
243  def GetNetworkData(self, package, timeout=None, retries=None):
244    """Get network data for specific package.
245
246    Args:
247      package: package name you want network data for.
248      timeout: timeout in seconds
249      retries: number of retries
250
251    Returns:
252      Tuple of (sent_data, recieved_data)
253      None if no network data found
254    """
255    # If device_utils clears cache, cache['uids'] doesn't exist
256    if 'uids' not in self._cache:
257      self._cache['uids'] = {}
258    if package not in self._cache['uids']:
259      self.GetPowerData()
260      if package not in self._cache['uids']:
261        logger.warning('No UID found for %s. Can\'t get network data.',
262                       package)
263        return None
264
265    network_data_path = '/proc/uid_stat/%s/' % self._cache['uids'][package]
266    try:
267      send_data = int(self._device.ReadFile(network_data_path + 'tcp_snd'))
268    # If ReadFile throws exception, it means no network data usage file for
269    # package has been recorded. Return 0 sent and 0 received.
270    except device_errors.AdbShellCommandFailedError:
271      logger.warning('No sent data found for package %s', package)
272      send_data = 0
273    try:
274      recv_data = int(self._device.ReadFile(network_data_path + 'tcp_rcv'))
275    except device_errors.AdbShellCommandFailedError:
276      logger.warning('No received data found for package %s', package)
277      recv_data = 0
278    return (send_data, recv_data)
279
280  @decorators.WithTimeoutAndRetriesFromInstance()
281  def GetPowerData(self, timeout=None, retries=None):
282    """Get power data for device.
283
284    Args:
285      timeout: timeout in seconds
286      retries: number of retries
287
288    Returns:
289      Dict containing system power, and a per-package power dict keyed on
290      package names.
291      {
292        'system_total': 23.1,
293        'per_package' : {
294          package_name: {
295            'uid': uid,
296            'data': [1,2,3]
297          },
298        }
299      }
300    """
301    if 'uids' not in self._cache:
302      self._cache['uids'] = {}
303    dumpsys_output = self._device.RunShellCommand(
304        ['dumpsys', 'batterystats', '-c'],
305        check_return=True, large_output=True)
306    csvreader = csv.reader(dumpsys_output)
307    pwi_entries = collections.defaultdict(list)
308    system_total = None
309    for entry in csvreader:
310      if entry[_DUMP_VERSION_INDEX] not in ['8', '9']:
311        # Wrong dumpsys version.
312        raise device_errors.DeviceVersionError(
313            'Dumpsys version must be 8 or 9. "%s" found.'
314            % entry[_DUMP_VERSION_INDEX])
315      if _ROW_TYPE_INDEX < len(entry) and entry[_ROW_TYPE_INDEX] == 'uid':
316        current_package = entry[_PACKAGE_NAME_INDEX]
317        if (self._cache['uids'].get(current_package)
318            and self._cache['uids'].get(current_package)
319            != entry[_PACKAGE_UID_INDEX]):
320          raise device_errors.CommandFailedError(
321              'Package %s found multiple times with different UIDs %s and %s'
322               % (current_package, self._cache['uids'][current_package],
323               entry[_PACKAGE_UID_INDEX]))
324        self._cache['uids'][current_package] = entry[_PACKAGE_UID_INDEX]
325      elif (_PWI_POWER_CONSUMPTION_INDEX < len(entry)
326          and entry[_ROW_TYPE_INDEX] == 'pwi'
327          and entry[_PWI_AGGREGATION_INDEX] == 'l'):
328        pwi_entries[entry[_PWI_UID_INDEX]].append(
329            float(entry[_PWI_POWER_CONSUMPTION_INDEX]))
330      elif (_PWS_POWER_CONSUMPTION_INDEX < len(entry)
331          and entry[_ROW_TYPE_INDEX] == 'pws'
332          and entry[_PWS_AGGREGATION_INDEX] == 'l'):
333        # This entry should only appear once.
334        assert system_total is None
335        system_total = float(entry[_PWS_POWER_CONSUMPTION_INDEX])
336
337    per_package = {p: {'uid': uid, 'data': pwi_entries[uid]}
338                   for p, uid in self._cache['uids'].iteritems()}
339    return {'system_total': system_total, 'per_package': per_package}
340
341  @decorators.WithTimeoutAndRetriesFromInstance()
342  def GetBatteryInfo(self, timeout=None, retries=None):
343    """Gets battery info for the device.
344
345    Args:
346      timeout: timeout in seconds
347      retries: number of retries
348    Returns:
349      A dict containing various battery information as reported by dumpsys
350      battery.
351    """
352    result = {}
353    # Skip the first line, which is just a header.
354    for line in self._device.RunShellCommand(
355        ['dumpsys', 'battery'], check_return=True)[1:]:
356      # If usb charging has been disabled, an extra line of header exists.
357      if 'UPDATES STOPPED' in line:
358        logger.warning('Dumpsys battery not receiving updates. '
359                       'Run dumpsys battery reset if this is in error.')
360      elif ':' not in line:
361        logger.warning('Unknown line found in dumpsys battery: "%s"', line)
362      else:
363        k, v = line.split(':', 1)
364        result[k.strip()] = v.strip()
365    return result
366
367  @decorators.WithTimeoutAndRetriesFromInstance()
368  def GetCharging(self, timeout=None, retries=None):
369    """Gets the charging state of the device.
370
371    Args:
372      timeout: timeout in seconds
373      retries: number of retries
374    Returns:
375      True if the device is charging, false otherwise.
376    """
377    battery_info = self.GetBatteryInfo()
378    for k in ('AC powered', 'USB powered', 'Wireless powered'):
379      if (k in battery_info and
380          battery_info[k].lower() in ('true', '1', 'yes')):
381        return True
382    return False
383
384  # TODO(rnephew): Make private when all use cases can use the context manager.
385  @decorators.WithTimeoutAndRetriesFromInstance()
386  def DisableBatteryUpdates(self, timeout=None, retries=None):
387    """Resets battery data and makes device appear like it is not
388    charging so that it will collect power data since last charge.
389
390    Args:
391      timeout: timeout in seconds
392      retries: number of retries
393
394    Raises:
395      device_errors.CommandFailedError: When resetting batterystats fails to
396        reset power values.
397      device_errors.DeviceVersionError: If device is not L or higher.
398    """
399    def battery_updates_disabled():
400      return self.GetCharging() is False
401
402    self._ClearPowerData()
403    self._device.RunShellCommand(['dumpsys', 'battery', 'set', 'ac', '0'],
404                                 check_return=True)
405    self._device.RunShellCommand(['dumpsys', 'battery', 'set', 'usb', '0'],
406                                 check_return=True)
407    timeout_retry.WaitFor(battery_updates_disabled, wait_period=1)
408
409  # TODO(rnephew): Make private when all use cases can use the context manager.
410  @decorators.WithTimeoutAndRetriesFromInstance()
411  def EnableBatteryUpdates(self, timeout=None, retries=None):
412    """Restarts device charging so that dumpsys no longer collects power data.
413
414    Args:
415      timeout: timeout in seconds
416      retries: number of retries
417
418    Raises:
419      device_errors.DeviceVersionError: If device is not L or higher.
420    """
421    def battery_updates_enabled():
422      return (self.GetCharging()
423              or not bool('UPDATES STOPPED' in self._device.RunShellCommand(
424                  ['dumpsys', 'battery'], check_return=True)))
425
426    self._device.RunShellCommand(['dumpsys', 'battery', 'reset'],
427                                 check_return=True)
428    timeout_retry.WaitFor(battery_updates_enabled, wait_period=1)
429
430  @contextlib.contextmanager
431  def BatteryMeasurement(self, timeout=None, retries=None):
432    """Context manager that enables battery data collection. It makes
433    the device appear to stop charging so that dumpsys will start collecting
434    power data since last charge. Once the with block is exited, charging is
435    resumed and power data since last charge is no longer collected.
436
437    Only for devices L and higher.
438
439    Example usage:
440      with BatteryMeasurement():
441        browser_actions()
442        get_power_data() # report usage within this block
443      after_measurements() # Anything that runs after power
444                           # measurements are collected
445
446    Args:
447      timeout: timeout in seconds
448      retries: number of retries
449
450    Raises:
451      device_errors.DeviceVersionError: If device is not L or higher.
452    """
453    if self._device.build_version_sdk < version_codes.LOLLIPOP:
454      raise device_errors.DeviceVersionError('Device must be L or higher.')
455    try:
456      self.DisableBatteryUpdates(timeout=timeout, retries=retries)
457      yield
458    finally:
459      self.EnableBatteryUpdates(timeout=timeout, retries=retries)
460
461  def _DischargeDevice(self, percent, wait_period=120):
462    """Disables charging and waits for device to discharge given amount
463
464    Args:
465      percent: level of charge to discharge.
466
467    Raises:
468      ValueError: If percent is not between 1 and 99.
469    """
470    battery_level = int(self.GetBatteryInfo().get('level'))
471    if not 0 < percent < 100:
472      raise ValueError('Discharge amount(%s) must be between 1 and 99'
473                       % percent)
474    if battery_level is None:
475      logger.warning('Unable to find current battery level. Cannot discharge.')
476      return
477    # Do not discharge if it would make battery level too low.
478    if percent >= battery_level - 10:
479      logger.warning('Battery is too low or discharge amount requested is too '
480                     'high. Cannot discharge phone %s percent.', percent)
481      return
482
483    self._HardwareSetCharging(False)
484
485    def device_discharged():
486      self._HardwareSetCharging(True)
487      current_level = int(self.GetBatteryInfo().get('level'))
488      logger.info('current battery level: %s', current_level)
489      if battery_level - current_level >= percent:
490        return True
491      self._HardwareSetCharging(False)
492      return False
493
494    timeout_retry.WaitFor(device_discharged, wait_period=wait_period)
495
496  def ChargeDeviceToLevel(self, level, wait_period=60):
497    """Enables charging and waits for device to be charged to given level.
498
499    Args:
500      level: level of charge to wait for.
501      wait_period: time in seconds to wait between checking.
502    Raises:
503      device_errors.DeviceChargingError: If error while charging is detected.
504    """
505    self.SetCharging(True)
506    charge_status = {
507        'charge_failure_count': 0,
508        'last_charge_value': 0
509    }
510    def device_charged():
511      battery_level = self.GetBatteryInfo().get('level')
512      if battery_level is None:
513        logger.warning('Unable to find current battery level.')
514        battery_level = 100
515      else:
516        logger.info('current battery level: %s', battery_level)
517        battery_level = int(battery_level)
518
519      # Use > so that it will not reset if charge is going down.
520      if battery_level > charge_status['last_charge_value']:
521        charge_status['last_charge_value'] = battery_level
522        charge_status['charge_failure_count'] = 0
523      else:
524        charge_status['charge_failure_count'] += 1
525
526      if (not battery_level >= level
527          and charge_status['charge_failure_count'] >= _MAX_CHARGE_ERROR):
528        raise device_errors.DeviceChargingError(
529            'Device not charging properly. Current level:%s Previous level:%s'
530             % (battery_level, charge_status['last_charge_value']))
531      return battery_level >= level
532
533    timeout_retry.WaitFor(device_charged, wait_period=wait_period)
534
535  def LetBatteryCoolToTemperature(self, target_temp, wait_period=180):
536    """Lets device sit to give battery time to cool down
537    Args:
538      temp: maximum temperature to allow in tenths of degrees c.
539      wait_period: time in seconds to wait between checking.
540    """
541    def cool_device():
542      temp = self.GetBatteryInfo().get('temperature')
543      if temp is None:
544        logger.warning('Unable to find current battery temperature.')
545        temp = 0
546      else:
547        logger.info('Current battery temperature: %s', temp)
548      if int(temp) <= target_temp:
549        return True
550      else:
551        if 'Nexus 5' in self._cache['profile']['name']:
552          self._DischargeDevice(1)
553        return False
554
555    self._DiscoverDeviceProfile()
556    self.EnableBatteryUpdates()
557    logger.info('Waiting for the device to cool down to %s (0.1 C)',
558                target_temp)
559    timeout_retry.WaitFor(cool_device, wait_period=wait_period)
560
561  @decorators.WithTimeoutAndRetriesFromInstance()
562  def SetCharging(self, enabled, timeout=None, retries=None):
563    """Enables or disables charging on the device.
564
565    Args:
566      enabled: A boolean indicating whether charging should be enabled or
567        disabled.
568      timeout: timeout in seconds
569      retries: number of retries
570    """
571    if self.GetCharging() == enabled:
572      logger.warning('Device charging already in expected state: %s', enabled)
573      return
574
575    self._DiscoverDeviceProfile()
576    if enabled:
577      if self._cache['profile']['enable_command']:
578        self._HardwareSetCharging(enabled)
579      else:
580        logger.info('Unable to enable charging via hardware. '
581                    'Falling back to software enabling.')
582        self.EnableBatteryUpdates()
583    else:
584      if self._cache['profile']['enable_command']:
585        self._ClearPowerData()
586        self._HardwareSetCharging(enabled)
587      else:
588        logger.info('Unable to disable charging via hardware. '
589                     'Falling back to software disabling.')
590        self.DisableBatteryUpdates()
591
592  def _HardwareSetCharging(self, enabled, timeout=None, retries=None):
593    """Enables or disables charging on the device.
594
595    Args:
596      enabled: A boolean indicating whether charging should be enabled or
597        disabled.
598      timeout: timeout in seconds
599      retries: number of retries
600
601    Raises:
602      device_errors.CommandFailedError: If method of disabling charging cannot
603        be determined.
604    """
605    self._DiscoverDeviceProfile()
606    if not self._cache['profile']['enable_command']:
607      raise device_errors.CommandFailedError(
608          'Unable to find charging commands.')
609
610    command = (self._cache['profile']['enable_command'] if enabled
611               else self._cache['profile']['disable_command'])
612
613    def verify_charging():
614      return self.GetCharging() == enabled
615
616    self._device.RunShellCommand(
617        command, shell=True, check_return=True, as_root=True, large_output=True)
618    timeout_retry.WaitFor(verify_charging, wait_period=1)
619
620  @contextlib.contextmanager
621  def PowerMeasurement(self, timeout=None, retries=None):
622    """Context manager that enables battery power collection.
623
624    Once the with block is exited, charging is resumed. Will attempt to disable
625    charging at the hardware level, and if that fails will fall back to software
626    disabling of battery updates.
627
628    Only for devices L and higher.
629
630    Example usage:
631      with PowerMeasurement():
632        browser_actions()
633        get_power_data() # report usage within this block
634      after_measurements() # Anything that runs after power
635                           # measurements are collected
636
637    Args:
638      timeout: timeout in seconds
639      retries: number of retries
640    """
641    try:
642      self.SetCharging(False, timeout=timeout, retries=retries)
643      yield
644    finally:
645      self.SetCharging(True, timeout=timeout, retries=retries)
646
647  def _ClearPowerData(self):
648    """Resets battery data and makes device appear like it is not
649    charging so that it will collect power data since last charge.
650
651    Returns:
652      True if power data cleared.
653      False if power data clearing is not supported (pre-L)
654
655    Raises:
656      device_errors.DeviceVersionError: If power clearing is supported,
657        but fails.
658    """
659    if self._device.build_version_sdk < version_codes.LOLLIPOP:
660      logger.warning('Dumpsys power data only available on 5.0 and above. '
661                     'Cannot clear power data.')
662      return False
663
664    self._device.RunShellCommand(
665        ['dumpsys', 'battery', 'set', 'usb', '1'], check_return=True)
666    self._device.RunShellCommand(
667        ['dumpsys', 'battery', 'set', 'ac', '1'], check_return=True)
668
669    def test_if_clear():
670      self._device.RunShellCommand(
671          ['dumpsys', 'batterystats', '--reset'], check_return=True)
672      battery_data = self._device.RunShellCommand(
673          ['dumpsys', 'batterystats', '--charged', '-c'],
674          check_return=True, large_output=True)
675      for line in battery_data:
676        l = line.split(',')
677        if (len(l) > _PWI_POWER_CONSUMPTION_INDEX
678            and l[_ROW_TYPE_INDEX] == 'pwi'
679            and float(l[_PWI_POWER_CONSUMPTION_INDEX]) != 0.0):
680          return False
681      return True
682
683    try:
684      timeout_retry.WaitFor(test_if_clear, wait_period=1)
685      return True
686    finally:
687      self._device.RunShellCommand(
688          ['dumpsys', 'battery', 'reset'], check_return=True)
689
690  def _DiscoverDeviceProfile(self):
691    """Checks and caches device information.
692
693    Returns:
694      True if profile is found, false otherwise.
695    """
696
697    if 'profile' in self._cache:
698      return True
699    for profile in _DEVICE_PROFILES:
700      if self._device.product_model in profile['name']:
701        self._cache['profile'] = profile
702        return True
703    self._cache['profile'] = {
704        'name': [],
705        'enable_command': None,
706        'disable_command': None,
707        'charge_counter': None,
708        'voltage': None,
709        'current': None,
710    }
711    return False
712