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

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

Powered by Google App Engine
This is Rietveld 408576698