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