Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(8)

Side by Side Diff: build/android/pylib/device/battery_utils.py

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

Powered by Google App Engine
This is Rietveld 408576698