| OLD | NEW |
| 1 # Copyright 2015 The Chromium Authors. All rights reserved. | 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """Provides a variety of device interactions with power. | 5 # pylint: disable=unused-wildcard-import |
| 6 """ | 6 # pylint: disable=wildcard-import |
| 7 # pylint: disable=unused-argument | |
| 8 | 7 |
| 9 import collections | 8 from devil.android.battery_utils import * |
| 10 import contextlib | |
| 11 import csv | |
| 12 import logging | |
| 13 | |
| 14 from pylib import constants | |
| 15 from pylib.device import decorators | |
| 16 from pylib.device import device_errors | |
| 17 from pylib.device import device_utils | |
| 18 from pylib.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 | |
| 100 # The list of useful dumpsys columns. | |
| 101 # Index of the column containing the format version. | |
| 102 _DUMP_VERSION_INDEX = 0 | |
| 103 # Index of the column containing the type of the row. | |
| 104 _ROW_TYPE_INDEX = 3 | |
| 105 # Index of the column containing the uid. | |
| 106 _PACKAGE_UID_INDEX = 4 | |
| 107 # Index of the column containing the application package. | |
| 108 _PACKAGE_NAME_INDEX = 5 | |
| 109 # The column containing the uid of the power data. | |
| 110 _PWI_UID_INDEX = 1 | |
| 111 # The column containing the type of consumption. Only consumption since last | |
| 112 # charge are of interest here. | |
| 113 _PWI_AGGREGATION_INDEX = 2 | |
| 114 _PWS_AGGREGATION_INDEX = _PWI_AGGREGATION_INDEX | |
| 115 # The column containing the amount of power used, in mah. | |
| 116 _PWI_POWER_CONSUMPTION_INDEX = 5 | |
| 117 _PWS_POWER_CONSUMPTION_INDEX = _PWI_POWER_CONSUMPTION_INDEX | |
| 118 | |
| 119 | |
| 120 class BatteryUtils(object): | |
| 121 | |
| 122 def __init__(self, device, default_timeout=_DEFAULT_TIMEOUT, | |
| 123 default_retries=_DEFAULT_RETRIES): | |
| 124 """BatteryUtils constructor. | |
| 125 | |
| 126 Args: | |
| 127 device: A DeviceUtils instance. | |
| 128 default_timeout: An integer containing the default number of seconds to | |
| 129 wait for an operation to complete if no explicit value | |
| 130 is provided. | |
| 131 default_retries: An integer containing the default number or times an | |
| 132 operation should be retried on failure if no explicit | |
| 133 value is provided. | |
| 134 | |
| 135 Raises: | |
| 136 TypeError: If it is not passed a DeviceUtils instance. | |
| 137 """ | |
| 138 if not isinstance(device, device_utils.DeviceUtils): | |
| 139 raise TypeError('Must be initialized with DeviceUtils object.') | |
| 140 self._device = device | |
| 141 self._cache = device.GetClientCache(self.__class__.__name__) | |
| 142 self._default_timeout = default_timeout | |
| 143 self._default_retries = default_retries | |
| 144 | |
| 145 @decorators.WithTimeoutAndRetriesFromInstance() | |
| 146 def SupportsFuelGauge(self, timeout=None, retries=None): | |
| 147 """Detect if fuel gauge chip is present. | |
| 148 | |
| 149 Args: | |
| 150 timeout: timeout in seconds | |
| 151 retries: number of retries | |
| 152 | |
| 153 Returns: | |
| 154 True if known fuel gauge files are present. | |
| 155 False otherwise. | |
| 156 """ | |
| 157 self._DiscoverDeviceProfile() | |
| 158 return (self._cache['profile']['enable_command'] != None | |
| 159 and self._cache['profile']['charge_counter'] != None) | |
| 160 | |
| 161 @decorators.WithTimeoutAndRetriesFromInstance() | |
| 162 def GetFuelGaugeChargeCounter(self, timeout=None, retries=None): | |
| 163 """Get value of charge_counter on fuel gauge chip. | |
| 164 | |
| 165 Device must have charging disabled for this, not just battery updates | |
| 166 disabled. The only device that this currently works with is the nexus 5. | |
| 167 | |
| 168 Args: | |
| 169 timeout: timeout in seconds | |
| 170 retries: number of retries | |
| 171 | |
| 172 Returns: | |
| 173 value of charge_counter for fuel gauge chip in units of nAh. | |
| 174 | |
| 175 Raises: | |
| 176 device_errors.CommandFailedError: If fuel gauge chip not found. | |
| 177 """ | |
| 178 if self.SupportsFuelGauge(): | |
| 179 return int(self._device.ReadFile( | |
| 180 self._cache['profile']['charge_counter'])) | |
| 181 raise device_errors.CommandFailedError( | |
| 182 'Unable to find fuel gauge.') | |
| 183 | |
| 184 @decorators.WithTimeoutAndRetriesFromInstance() | |
| 185 def GetNetworkData(self, package, timeout=None, retries=None): | |
| 186 """Get network data for specific package. | |
| 187 | |
| 188 Args: | |
| 189 package: package name you want network data for. | |
| 190 timeout: timeout in seconds | |
| 191 retries: number of retries | |
| 192 | |
| 193 Returns: | |
| 194 Tuple of (sent_data, recieved_data) | |
| 195 None if no network data found | |
| 196 """ | |
| 197 # If device_utils clears cache, cache['uids'] doesn't exist | |
| 198 if 'uids' not in self._cache: | |
| 199 self._cache['uids'] = {} | |
| 200 if package not in self._cache['uids']: | |
| 201 self.GetPowerData() | |
| 202 if package not in self._cache['uids']: | |
| 203 logging.warning('No UID found for %s. Can\'t get network data.', | |
| 204 package) | |
| 205 return None | |
| 206 | |
| 207 network_data_path = '/proc/uid_stat/%s/' % self._cache['uids'][package] | |
| 208 try: | |
| 209 send_data = int(self._device.ReadFile(network_data_path + 'tcp_snd')) | |
| 210 # If ReadFile throws exception, it means no network data usage file for | |
| 211 # package has been recorded. Return 0 sent and 0 received. | |
| 212 except device_errors.AdbShellCommandFailedError: | |
| 213 logging.warning('No sent data found for package %s', package) | |
| 214 send_data = 0 | |
| 215 try: | |
| 216 recv_data = int(self._device.ReadFile(network_data_path + 'tcp_rcv')) | |
| 217 except device_errors.AdbShellCommandFailedError: | |
| 218 logging.warning('No received data found for package %s', package) | |
| 219 recv_data = 0 | |
| 220 return (send_data, recv_data) | |
| 221 | |
| 222 @decorators.WithTimeoutAndRetriesFromInstance() | |
| 223 def GetPowerData(self, timeout=None, retries=None): | |
| 224 """Get power data for device. | |
| 225 | |
| 226 Args: | |
| 227 timeout: timeout in seconds | |
| 228 retries: number of retries | |
| 229 | |
| 230 Returns: | |
| 231 Dict containing system power, and a per-package power dict keyed on | |
| 232 package names. | |
| 233 { | |
| 234 'system_total': 23.1, | |
| 235 'per_package' : { | |
| 236 package_name: { | |
| 237 'uid': uid, | |
| 238 'data': [1,2,3] | |
| 239 }, | |
| 240 } | |
| 241 } | |
| 242 """ | |
| 243 if 'uids' not in self._cache: | |
| 244 self._cache['uids'] = {} | |
| 245 dumpsys_output = self._device.RunShellCommand( | |
| 246 ['dumpsys', 'batterystats', '-c'], | |
| 247 check_return=True, large_output=True) | |
| 248 csvreader = csv.reader(dumpsys_output) | |
| 249 pwi_entries = collections.defaultdict(list) | |
| 250 system_total = None | |
| 251 for entry in csvreader: | |
| 252 if entry[_DUMP_VERSION_INDEX] not in ['8', '9']: | |
| 253 # Wrong dumpsys version. | |
| 254 raise device_errors.DeviceVersionError( | |
| 255 'Dumpsys version must be 8 or 9. %s found.' | |
| 256 % entry[_DUMP_VERSION_INDEX]) | |
| 257 if _ROW_TYPE_INDEX < len(entry) and entry[_ROW_TYPE_INDEX] == 'uid': | |
| 258 current_package = entry[_PACKAGE_NAME_INDEX] | |
| 259 if (self._cache['uids'].get(current_package) | |
| 260 and self._cache['uids'].get(current_package) | |
| 261 != entry[_PACKAGE_UID_INDEX]): | |
| 262 raise device_errors.CommandFailedError( | |
| 263 'Package %s found multiple times with different UIDs %s and %s' | |
| 264 % (current_package, self._cache['uids'][current_package], | |
| 265 entry[_PACKAGE_UID_INDEX])) | |
| 266 self._cache['uids'][current_package] = entry[_PACKAGE_UID_INDEX] | |
| 267 elif (_PWI_POWER_CONSUMPTION_INDEX < len(entry) | |
| 268 and entry[_ROW_TYPE_INDEX] == 'pwi' | |
| 269 and entry[_PWI_AGGREGATION_INDEX] == 'l'): | |
| 270 pwi_entries[entry[_PWI_UID_INDEX]].append( | |
| 271 float(entry[_PWI_POWER_CONSUMPTION_INDEX])) | |
| 272 elif (_PWS_POWER_CONSUMPTION_INDEX < len(entry) | |
| 273 and entry[_ROW_TYPE_INDEX] == 'pws' | |
| 274 and entry[_PWS_AGGREGATION_INDEX] == 'l'): | |
| 275 # This entry should only appear once. | |
| 276 assert system_total is None | |
| 277 system_total = float(entry[_PWS_POWER_CONSUMPTION_INDEX]) | |
| 278 | |
| 279 per_package = {p: {'uid': uid, 'data': pwi_entries[uid]} | |
| 280 for p, uid in self._cache['uids'].iteritems()} | |
| 281 return {'system_total': system_total, 'per_package': per_package} | |
| 282 | |
| 283 @decorators.WithTimeoutAndRetriesFromInstance() | |
| 284 def GetBatteryInfo(self, timeout=None, retries=None): | |
| 285 """Gets battery info for the device. | |
| 286 | |
| 287 Args: | |
| 288 timeout: timeout in seconds | |
| 289 retries: number of retries | |
| 290 Returns: | |
| 291 A dict containing various battery information as reported by dumpsys | |
| 292 battery. | |
| 293 """ | |
| 294 result = {} | |
| 295 # Skip the first line, which is just a header. | |
| 296 for line in self._device.RunShellCommand( | |
| 297 ['dumpsys', 'battery'], check_return=True)[1:]: | |
| 298 # If usb charging has been disabled, an extra line of header exists. | |
| 299 if 'UPDATES STOPPED' in line: | |
| 300 logging.warning('Dumpsys battery not receiving updates. ' | |
| 301 'Run dumpsys battery reset if this is in error.') | |
| 302 elif ':' not in line: | |
| 303 logging.warning('Unknown line found in dumpsys battery: "%s"', line) | |
| 304 else: | |
| 305 k, v = line.split(':', 1) | |
| 306 result[k.strip()] = v.strip() | |
| 307 return result | |
| 308 | |
| 309 @decorators.WithTimeoutAndRetriesFromInstance() | |
| 310 def GetCharging(self, timeout=None, retries=None): | |
| 311 """Gets the charging state of the device. | |
| 312 | |
| 313 Args: | |
| 314 timeout: timeout in seconds | |
| 315 retries: number of retries | |
| 316 Returns: | |
| 317 True if the device is charging, false otherwise. | |
| 318 """ | |
| 319 battery_info = self.GetBatteryInfo() | |
| 320 for k in ('AC powered', 'USB powered', 'Wireless powered'): | |
| 321 if (k in battery_info and | |
| 322 battery_info[k].lower() in ('true', '1', 'yes')): | |
| 323 return True | |
| 324 return False | |
| 325 | |
| 326 @decorators.WithTimeoutAndRetriesFromInstance() | |
| 327 def SetCharging(self, enabled, timeout=None, retries=None): | |
| 328 """Enables or disables charging on the device. | |
| 329 | |
| 330 Args: | |
| 331 enabled: A boolean indicating whether charging should be enabled or | |
| 332 disabled. | |
| 333 timeout: timeout in seconds | |
| 334 retries: number of retries | |
| 335 | |
| 336 Raises: | |
| 337 device_errors.CommandFailedError: If method of disabling charging cannot | |
| 338 be determined. | |
| 339 """ | |
| 340 self._DiscoverDeviceProfile() | |
| 341 if not self._cache['profile']['enable_command']: | |
| 342 raise device_errors.CommandFailedError( | |
| 343 'Unable to find charging commands.') | |
| 344 | |
| 345 if enabled: | |
| 346 command = self._cache['profile']['enable_command'] | |
| 347 else: | |
| 348 command = self._cache['profile']['disable_command'] | |
| 349 | |
| 350 def verify_charging(): | |
| 351 return self.GetCharging() == enabled | |
| 352 | |
| 353 self._device.RunShellCommand( | |
| 354 command, check_return=True, as_root=True, large_output=True) | |
| 355 timeout_retry.WaitFor(verify_charging, wait_period=1) | |
| 356 | |
| 357 # TODO(rnephew): Make private when all use cases can use the context manager. | |
| 358 @decorators.WithTimeoutAndRetriesFromInstance() | |
| 359 def DisableBatteryUpdates(self, timeout=None, retries=None): | |
| 360 """Resets battery data and makes device appear like it is not | |
| 361 charging so that it will collect power data since last charge. | |
| 362 | |
| 363 Args: | |
| 364 timeout: timeout in seconds | |
| 365 retries: number of retries | |
| 366 | |
| 367 Raises: | |
| 368 device_errors.CommandFailedError: When resetting batterystats fails to | |
| 369 reset power values. | |
| 370 device_errors.DeviceVersionError: If device is not L or higher. | |
| 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 def battery_updates_enabled(): | |
| 395 return (self.GetCharging() | |
| 396 or not bool('UPDATES STOPPED' in self._device.RunShellCommand( | |
| 397 ['dumpsys', 'battery'], check_return=True))) | |
| 398 | |
| 399 self._device.RunShellCommand(['dumpsys', 'battery', 'reset'], | |
| 400 check_return=True) | |
| 401 timeout_retry.WaitFor(battery_updates_enabled, wait_period=1) | |
| 402 | |
| 403 @contextlib.contextmanager | |
| 404 def BatteryMeasurement(self, timeout=None, retries=None): | |
| 405 """Context manager that enables battery data collection. It makes | |
| 406 the device appear to stop charging so that dumpsys will start collecting | |
| 407 power data since last charge. Once the with block is exited, charging is | |
| 408 resumed and power data since last charge is no longer collected. | |
| 409 | |
| 410 Only for devices L and higher. | |
| 411 | |
| 412 Example usage: | |
| 413 with BatteryMeasurement(): | |
| 414 browser_actions() | |
| 415 get_power_data() # report usage within this block | |
| 416 after_measurements() # Anything that runs after power | |
| 417 # measurements are collected | |
| 418 | |
| 419 Args: | |
| 420 timeout: timeout in seconds | |
| 421 retries: number of retries | |
| 422 | |
| 423 Raises: | |
| 424 device_errors.DeviceVersionError: If device is not L or higher. | |
| 425 """ | |
| 426 if (self._device.build_version_sdk < | |
| 427 constants.ANDROID_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('Discharge amount(%s) must be between 1 and 99' | |
| 447 % percent) | |
| 448 if battery_level is None: | |
| 449 logging.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 logging.warning('Battery is too low or discharge amount requested is too ' | |
| 454 'high. Cannot discharge phone %s percent.', percent) | |
| 455 return | |
| 456 | |
| 457 self.SetCharging(False) | |
| 458 def device_discharged(): | |
| 459 self.SetCharging(True) | |
| 460 current_level = int(self.GetBatteryInfo().get('level')) | |
| 461 logging.info('current battery level: %s', current_level) | |
| 462 if battery_level - current_level >= percent: | |
| 463 return True | |
| 464 self.SetCharging(False) | |
| 465 return False | |
| 466 | |
| 467 timeout_retry.WaitFor(device_discharged, wait_period=wait_period) | |
| 468 | |
| 469 def ChargeDeviceToLevel(self, level, wait_period=60): | |
| 470 """Enables charging and waits for device to be charged to given level. | |
| 471 | |
| 472 Args: | |
| 473 level: level of charge to wait for. | |
| 474 wait_period: time in seconds to wait between checking. | |
| 475 """ | |
| 476 self.SetCharging(True) | |
| 477 | |
| 478 def device_charged(): | |
| 479 battery_level = self.GetBatteryInfo().get('level') | |
| 480 if battery_level is None: | |
| 481 logging.warning('Unable to find current battery level.') | |
| 482 battery_level = 100 | |
| 483 else: | |
| 484 logging.info('current battery level: %s', battery_level) | |
| 485 battery_level = int(battery_level) | |
| 486 return battery_level >= level | |
| 487 | |
| 488 timeout_retry.WaitFor(device_charged, wait_period=wait_period) | |
| 489 | |
| 490 def LetBatteryCoolToTemperature(self, target_temp, wait_period=180): | |
| 491 """Lets device sit to give battery time to cool down | |
| 492 Args: | |
| 493 temp: maximum temperature to allow in tenths of degrees c. | |
| 494 wait_period: time in seconds to wait between checking. | |
| 495 """ | |
| 496 def cool_device(): | |
| 497 temp = self.GetBatteryInfo().get('temperature') | |
| 498 if temp is None: | |
| 499 logging.warning('Unable to find current battery temperature.') | |
| 500 temp = 0 | |
| 501 else: | |
| 502 logging.info('Current battery temperature: %s', temp) | |
| 503 if int(temp) <= target_temp: | |
| 504 return True | |
| 505 else: | |
| 506 if self._cache['profile']['name'] == 'Nexus 5': | |
| 507 self._DischargeDevice(1) | |
| 508 return False | |
| 509 | |
| 510 self._DiscoverDeviceProfile() | |
| 511 self.EnableBatteryUpdates() | |
| 512 logging.info('Waiting for the device to cool down to %s (0.1 C)', | |
| 513 target_temp) | |
| 514 timeout_retry.WaitFor(cool_device, wait_period=wait_period) | |
| 515 | |
| 516 @decorators.WithTimeoutAndRetriesFromInstance() | |
| 517 def TieredSetCharging(self, enabled, timeout=None, retries=None): | |
| 518 """Enables or disables charging on the device. | |
| 519 | |
| 520 Args: | |
| 521 enabled: A boolean indicating whether charging should be enabled or | |
| 522 disabled. | |
| 523 timeout: timeout in seconds | |
| 524 retries: number of retries | |
| 525 """ | |
| 526 if self.GetCharging() == enabled: | |
| 527 logging.warning('Device charging already in expected state: %s', enabled) | |
| 528 return | |
| 529 | |
| 530 self._DiscoverDeviceProfile() | |
| 531 if enabled: | |
| 532 if self._cache['profile']['enable_command']: | |
| 533 self.SetCharging(enabled) | |
| 534 else: | |
| 535 logging.info('Unable to enable charging via hardware. ' | |
| 536 'Falling back to software enabling.') | |
| 537 self.EnableBatteryUpdates() | |
| 538 else: | |
| 539 if self._cache['profile']['enable_command']: | |
| 540 self._ClearPowerData() | |
| 541 self.SetCharging(enabled) | |
| 542 else: | |
| 543 logging.info('Unable to disable charging via hardware. ' | |
| 544 'Falling back to software disabling.') | |
| 545 self.DisableBatteryUpdates() | |
| 546 | |
| 547 @contextlib.contextmanager | |
| 548 def PowerMeasurement(self, timeout=None, retries=None): | |
| 549 """Context manager that enables battery power collection. | |
| 550 | |
| 551 Once the with block is exited, charging is resumed. Will attempt to disable | |
| 552 charging at the hardware level, and if that fails will fall back to software | |
| 553 disabling of battery updates. | |
| 554 | |
| 555 Only for devices L and higher. | |
| 556 | |
| 557 Example usage: | |
| 558 with PowerMeasurement(): | |
| 559 browser_actions() | |
| 560 get_power_data() # report usage within this block | |
| 561 after_measurements() # Anything that runs after power | |
| 562 # measurements are collected | |
| 563 | |
| 564 Args: | |
| 565 timeout: timeout in seconds | |
| 566 retries: number of retries | |
| 567 """ | |
| 568 try: | |
| 569 self.TieredSetCharging(False, timeout=timeout, retries=retries) | |
| 570 yield | |
| 571 finally: | |
| 572 self.TieredSetCharging(True, timeout=timeout, retries=retries) | |
| 573 | |
| 574 def _ClearPowerData(self): | |
| 575 """Resets battery data and makes device appear like it is not | |
| 576 charging so that it will collect power data since last charge. | |
| 577 | |
| 578 Returns: | |
| 579 True if power data cleared. | |
| 580 False if power data clearing is not supported (pre-L) | |
| 581 | |
| 582 Raises: | |
| 583 device_errors.DeviceVersionError: If power clearing is supported, | |
| 584 but fails. | |
| 585 """ | |
| 586 if (self._device.build_version_sdk < | |
| 587 constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP): | |
| 588 logging.warning('Dumpsys power data only available on 5.0 and above. ' | |
| 589 'Cannot clear power data.') | |
| 590 return False | |
| 591 | |
| 592 self._device.RunShellCommand( | |
| 593 ['dumpsys', 'battery', 'set', 'usb', '1'], check_return=True) | |
| 594 self._device.RunShellCommand( | |
| 595 ['dumpsys', 'battery', 'set', 'ac', '1'], check_return=True) | |
| 596 self._device.RunShellCommand( | |
| 597 ['dumpsys', 'batterystats', '--reset'], check_return=True) | |
| 598 battery_data = self._device.RunShellCommand( | |
| 599 ['dumpsys', 'batterystats', '--charged', '-c'], | |
| 600 check_return=True, large_output=True) | |
| 601 for line in battery_data: | |
| 602 l = line.split(',') | |
| 603 if (len(l) > _PWI_POWER_CONSUMPTION_INDEX and l[_ROW_TYPE_INDEX] == 'pwi' | |
| 604 and l[_PWI_POWER_CONSUMPTION_INDEX] != 0): | |
| 605 self._device.RunShellCommand( | |
| 606 ['dumpsys', 'battery', 'reset'], check_return=True) | |
| 607 raise device_errors.CommandFailedError( | |
| 608 'Non-zero pmi value found after reset.') | |
| 609 self._device.RunShellCommand( | |
| 610 ['dumpsys', 'battery', 'reset'], check_return=True) | |
| 611 return True | |
| 612 | |
| 613 def _DiscoverDeviceProfile(self): | |
| 614 """Checks and caches device information. | |
| 615 | |
| 616 Returns: | |
| 617 True if profile is found, false otherwise. | |
| 618 """ | |
| 619 | |
| 620 if 'profile' in self._cache: | |
| 621 return True | |
| 622 for profile in _DEVICE_PROFILES: | |
| 623 if self._device.product_model == profile['name']: | |
| 624 self._cache['profile'] = profile | |
| 625 return True | |
| 626 self._cache['profile'] = { | |
| 627 'name': None, | |
| 628 'witness_file': None, | |
| 629 'enable_command': None, | |
| 630 'disable_command': None, | |
| 631 'charge_counter': None, | |
| 632 'voltage': None, | |
| 633 'current': None, | |
| 634 } | |
| 635 return False | |
| OLD | NEW |