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