blob: c41c19a201bfbfa4be9a54c1754b2e575e2da4f3 [file] [log] [blame]
Justin Klaassende05df42016-11-22 15:38:08 -08001# 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
Oussama Ben Abdelbakidcd74cf2020-08-10 14:00:36 -040014from devil.android import crash_handler
Justin Klaassende05df42016-11-22 15:38:08 -080015from devil.android import decorators
16from devil.android import device_errors
17from devil.android import device_utils
18from devil.android.sdk import version_codes
19from devil.utils import timeout_retry
20
Justin Klaassen82688992016-11-22 19:09:02 -080021logger = logging.getLogger(__name__)
22
Justin Klaassende05df42016-11-22 15:38:08 -080023_DEFAULT_TIMEOUT = 30
24_DEFAULT_RETRIES = 3
25
26
27_DEVICE_PROFILES = [
28 {
Justin Klaassen82688992016-11-22 19:09:02 -080029 'name': ['Nexus 4'],
Justin Klaassende05df42016-11-22 15:38:08 -080030 '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 Klaassen82688992016-11-22 19:09:02 -080041 'name': ['Nexus 5'],
Justin Klaassende05df42016-11-22 15:38:08 -080042 # 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 Klaassende05df42016-11-22 15:38:08 -080046 '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 Klaassen82688992016-11-22 19:09:02 -080061 'name': ['Nexus 6'],
Justin Klaassende05df42016-11-22 15:38:08 -080062 '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 Klaassen82688992016-11-22 19:09:02 -080074 'name': ['Nexus 9'],
Justin Klaassende05df42016-11-22 15:38:08 -080075 '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 Klaassen82688992016-11-22 19:09:02 -080088 'name': ['Nexus 10'],
Justin Klaassende05df42016-11-22 15:38:08 -080089 '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 Klaassen82688992016-11-22 19:09:02 -080097 'name': ['Nexus 5X'],
Justin Klaassende05df42016-11-22 15:38:08 -080098 '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 Klaassen82688992016-11-22 19:09:02 -0800108 { # 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 Klaassenff093d52017-08-07 14:19:47 -0400144 { # 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 Klaassende05df42016-11-22 15:38:08 -0800156]
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
180class 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 Klaassende05df42016-11-22 15:38:08 -0800244 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 Klaassen82688992016-11-22 19:09:02 -0800321 logger.warning('Dumpsys battery not receiving updates. '
322 'Run dumpsys battery reset if this is in error.')
Justin Klaassende05df42016-11-22 15:38:08 -0800323 elif ':' not in line:
Justin Klaassen82688992016-11-22 19:09:02 -0800324 logger.warning('Unknown line found in dumpsys battery: "%s"', line)
Justin Klaassende05df42016-11-22 15:38:08 -0800325 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 Abdelbakidcd74cf2020-08-10 14:00:36 -0400340 # 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 Klaassende05df42016-11-22 15:38:08 -0800346 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 Klaassen82688992016-11-22 19:09:02 -0800443 logger.warning('Unable to find current battery level. Cannot discharge.')
Justin Klaassende05df42016-11-22 15:38:08 -0800444 return
445 # Do not discharge if it would make battery level too low.
446 if percent >= battery_level - 10:
Justin Klaassen82688992016-11-22 19:09:02 -0800447 logger.warning('Battery is too low or discharge amount requested is too '
448 'high. Cannot discharge phone %s percent.', percent)
Justin Klaassende05df42016-11-22 15:38:08 -0800449 return
450
451 self._HardwareSetCharging(False)
452
453 def device_discharged():
454 self._HardwareSetCharging(True)
455 current_level = int(self.GetBatteryInfo().get('level'))
Justin Klaassen82688992016-11-22 19:09:02 -0800456 logger.info('current battery level: %s', current_level)
Justin Klaassende05df42016-11-22 15:38:08 -0800457 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 Klaassen82688992016-11-22 19:09:02 -0800481 logger.warning('Unable to find current battery level.')
Justin Klaassende05df42016-11-22 15:38:08 -0800482 battery_level = 100
483 else:
Justin Klaassen82688992016-11-22 19:09:02 -0800484 logger.info('current battery level: %s', battery_level)
Justin Klaassende05df42016-11-22 15:38:08 -0800485 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 Klaassen82688992016-11-22 19:09:02 -0800512 logger.warning('Unable to find current battery temperature.')
Justin Klaassende05df42016-11-22 15:38:08 -0800513 temp = 0
514 else:
Justin Klaassen82688992016-11-22 19:09:02 -0800515 logger.info('Current battery temperature: %s', temp)
Justin Klaassende05df42016-11-22 15:38:08 -0800516 if int(temp) <= target_temp:
517 return True
518 else:
Justin Klaassen82688992016-11-22 19:09:02 -0800519 if 'Nexus 5' in self._cache['profile']['name']:
Justin Klaassende05df42016-11-22 15:38:08 -0800520 self._DischargeDevice(1)
521 return False
522
523 self._DiscoverDeviceProfile()
524 self.EnableBatteryUpdates()
Justin Klaassen82688992016-11-22 19:09:02 -0800525 logger.info('Waiting for the device to cool down to %s (0.1 C)',
526 target_temp)
Justin Klaassende05df42016-11-22 15:38:08 -0800527 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 Klaassen82688992016-11-22 19:09:02 -0800540 logger.warning('Device charging already in expected state: %s', enabled)
Justin Klaassende05df42016-11-22 15:38:08 -0800541 return
542
543 self._DiscoverDeviceProfile()
544 if enabled:
545 if self._cache['profile']['enable_command']:
546 self._HardwareSetCharging(enabled)
547 else:
Justin Klaassen82688992016-11-22 19:09:02 -0800548 logger.info('Unable to enable charging via hardware. '
549 'Falling back to software enabling.')
Justin Klaassende05df42016-11-22 15:38:08 -0800550 self.EnableBatteryUpdates()
551 else:
552 if self._cache['profile']['enable_command']:
553 self._ClearPowerData()
554 self._HardwareSetCharging(enabled)
555 else:
Justin Klaassen82688992016-11-22 19:09:02 -0800556 logger.info('Unable to disable charging via hardware. '
Justin Klaassende05df42016-11-22 15:38:08 -0800557 '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 Klaassen6129c132018-04-04 00:14:34 -0400585 command, shell=True, check_return=True, as_root=True, large_output=True)
Justin Klaassende05df42016-11-22 15:38:08 -0800586 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 Klaassen82688992016-11-22 19:09:02 -0800628 logger.warning('Dumpsys power data only available on 5.0 and above. '
629 'Cannot clear power data.')
Justin Klaassende05df42016-11-22 15:38:08 -0800630 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 Klaassen82688992016-11-22 19:09:02 -0800668 if self._device.product_model in profile['name']:
Justin Klaassende05df42016-11-22 15:38:08 -0800669 self._cache['profile'] = profile
670 return True
671 self._cache['profile'] = {
Justin Klaassen82688992016-11-22 19:09:02 -0800672 'name': [],
Justin Klaassende05df42016-11-22 15:38:08 -0800673 'enable_command': None,
674 'disable_command': None,
675 'charge_counter': None,
676 'voltage': None,
677 'current': None,
678 }
679 return False