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