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

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

Issue 2101243005: Add a snapshot of flutter/engine/src/build to our sdk (Closed) Base URL: git@github.com:dart-lang/sdk.git@master
Patch Set: add README.dart Created 4 years, 5 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
(Empty)
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
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 'echo 1 > /sys/class/power_supply/usb/online &&'
48 'dumpsys battery reset'),
49 'disable_command': (
50 'echo 0xCA > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && '
51 'chmod 644 /sys/class/power_supply/usb/online && '
52 'echo 0 > /sys/class/power_supply/usb/online && '
53 'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
54 'charge_counter': None,
55 'voltage': None,
56 'current': None,
57 },
58 {
59 'name': 'Nexus 6',
60 'witness_file': None,
61 'enable_command': (
62 'echo 1 > /sys/class/power_supply/battery/charging_enabled && '
63 'dumpsys battery reset'),
64 'disable_command': (
65 'echo 0 > /sys/class/power_supply/battery/charging_enabled && '
66 'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
67 'charge_counter': (
68 '/sys/class/power_supply/max170xx_battery/charge_counter_ext'),
69 'voltage': '/sys/class/power_supply/max170xx_battery/voltage_now',
70 'current': '/sys/class/power_supply/max170xx_battery/current_now',
71 },
72 {
73 'name': 'Nexus 9',
74 'witness_file': None,
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 {
88 'name': 'Nexus 10',
89 'witness_file': None,
90 'enable_command': None,
91 'disable_command': None,
92 'charge_counter': None,
93 'voltage': '/sys/class/power_supply/ds2784-fuelgauge/voltage_now',
94 'current': '/sys/class/power_supply/ds2784-fuelgauge/current_now',
95
96 },
97 ]
98
99 # The list of useful dumpsys columns.
100 # Index of the column containing the format version.
101 _DUMP_VERSION_INDEX = 0
102 # Index of the column containing the type of the row.
103 _ROW_TYPE_INDEX = 3
104 # Index of the column containing the uid.
105 _PACKAGE_UID_INDEX = 4
106 # Index of the column containing the application package.
107 _PACKAGE_NAME_INDEX = 5
108 # The column containing the uid of the power data.
109 _PWI_UID_INDEX = 1
110 # The column containing the type of consumption. Only consumtion since last
111 # charge are of interest here.
112 _PWI_AGGREGATION_INDEX = 2
113 # The column containing the amount of power used, in mah.
114 _PWI_POWER_CONSUMPTION_INDEX = 5
115
116
117 class BatteryUtils(object):
118
119 def __init__(self, device, default_timeout=_DEFAULT_TIMEOUT,
120 default_retries=_DEFAULT_RETRIES):
121 """BatteryUtils constructor.
122
123 Args:
124 device: A DeviceUtils instance.
125 default_timeout: An integer containing the default number of seconds to
126 wait for an operation to complete if no explicit value
127 is provided.
128 default_retries: An integer containing the default number or times an
129 operation should be retried on failure if no explicit
130 value is provided.
131
132 Raises:
133 TypeError: If it is not passed a DeviceUtils instance.
134 """
135 if not isinstance(device, device_utils.DeviceUtils):
136 raise TypeError('Must be initialized with DeviceUtils object.')
137 self._device = device
138 self._cache = device.GetClientCache(self.__class__.__name__)
139 self._default_timeout = default_timeout
140 self._default_retries = default_retries
141
142 @decorators.WithTimeoutAndRetriesFromInstance()
143 def SupportsFuelGauge(self, timeout=None, retries=None):
144 """Detect if fuel gauge chip is present.
145
146 Args:
147 timeout: timeout in seconds
148 retries: number of retries
149
150 Returns:
151 True if known fuel gauge files are present.
152 False otherwise.
153 """
154 self._DiscoverDeviceProfile()
155 return (self._cache['profile']['enable_command'] != None
156 and self._cache['profile']['charge_counter'] != None)
157
158 @decorators.WithTimeoutAndRetriesFromInstance()
159 def GetFuelGaugeChargeCounter(self, timeout=None, retries=None):
160 """Get value of charge_counter on fuel gauge chip.
161
162 Device must have charging disabled for this, not just battery updates
163 disabled. The only device that this currently works with is the nexus 5.
164
165 Args:
166 timeout: timeout in seconds
167 retries: number of retries
168
169 Returns:
170 value of charge_counter for fuel gauge chip in units of nAh.
171
172 Raises:
173 device_errors.CommandFailedError: If fuel gauge chip not found.
174 """
175 if self.SupportsFuelGauge():
176 return int(self._device.ReadFile(
177 self._cache['profile']['charge_counter']))
178 raise device_errors.CommandFailedError(
179 'Unable to find fuel gauge.')
180
181 @decorators.WithTimeoutAndRetriesFromInstance()
182 def GetNetworkData(self, package, timeout=None, retries=None):
183 """Get network data for specific package.
184
185 Args:
186 package: package name you want network data for.
187 timeout: timeout in seconds
188 retries: number of retries
189
190 Returns:
191 Tuple of (sent_data, recieved_data)
192 None if no network data found
193 """
194 # If device_utils clears cache, cache['uids'] doesn't exist
195 if 'uids' not in self._cache:
196 self._cache['uids'] = {}
197 if package not in self._cache['uids']:
198 self.GetPowerData()
199 if package not in self._cache['uids']:
200 logging.warning('No UID found for %s. Can\'t get network data.',
201 package)
202 return None
203
204 network_data_path = '/proc/uid_stat/%s/' % self._cache['uids'][package]
205 try:
206 send_data = int(self._device.ReadFile(network_data_path + 'tcp_snd'))
207 # If ReadFile throws exception, it means no network data usage file for
208 # package has been recorded. Return 0 sent and 0 received.
209 except device_errors.AdbShellCommandFailedError:
210 logging.warning('No sent data found for package %s', package)
211 send_data = 0
212 try:
213 recv_data = int(self._device.ReadFile(network_data_path + 'tcp_rcv'))
214 except device_errors.AdbShellCommandFailedError:
215 logging.warning('No received data found for package %s', package)
216 recv_data = 0
217 return (send_data, recv_data)
218
219 @decorators.WithTimeoutAndRetriesFromInstance()
220 def GetPowerData(self, timeout=None, retries=None):
221 """Get power data for device.
222
223 Args:
224 timeout: timeout in seconds
225 retries: number of retries
226
227 Returns:
228 Dict of power data, keyed on package names.
229 {
230 package_name: {
231 'uid': uid,
232 'data': [1,2,3]
233 },
234 }
235 """
236 if 'uids' not in self._cache:
237 self._cache['uids'] = {}
238 dumpsys_output = self._device.RunShellCommand(
239 ['dumpsys', 'batterystats', '-c'], check_return=True)
240 csvreader = csv.reader(dumpsys_output)
241 pwi_entries = collections.defaultdict(list)
242 for entry in csvreader:
243 if entry[_DUMP_VERSION_INDEX] not in ['8', '9']:
244 # Wrong dumpsys version.
245 raise device_errors.DeviceVersionError(
246 'Dumpsys version must be 8 or 9. %s found.'
247 % entry[_DUMP_VERSION_INDEX])
248 if _ROW_TYPE_INDEX < len(entry) and entry[_ROW_TYPE_INDEX] == 'uid':
249 current_package = entry[_PACKAGE_NAME_INDEX]
250 if (self._cache['uids'].get(current_package)
251 and self._cache['uids'].get(current_package)
252 != entry[_PACKAGE_UID_INDEX]):
253 raise device_errors.CommandFailedError(
254 'Package %s found multiple times with differnt UIDs %s and %s'
255 % (current_package, self._cache['uids'][current_package],
256 entry[_PACKAGE_UID_INDEX]))
257 self._cache['uids'][current_package] = entry[_PACKAGE_UID_INDEX]
258 elif (_PWI_POWER_CONSUMPTION_INDEX < len(entry)
259 and entry[_ROW_TYPE_INDEX] == 'pwi'
260 and entry[_PWI_AGGREGATION_INDEX] == 'l'):
261 pwi_entries[entry[_PWI_UID_INDEX]].append(
262 float(entry[_PWI_POWER_CONSUMPTION_INDEX]))
263
264 return {p: {'uid': uid, 'data': pwi_entries[uid]}
265 for p, uid in self._cache['uids'].iteritems()}
266
267 @decorators.WithTimeoutAndRetriesFromInstance()
268 def GetPackagePowerData(self, package, timeout=None, retries=None):
269 """Get power data for particular package.
270
271 Args:
272 package: Package to get power data on.
273
274 returns:
275 Dict of UID and power data.
276 {
277 'uid': uid,
278 'data': [1,2,3]
279 }
280 None if the package is not found in the power data.
281 """
282 return self.GetPowerData().get(package)
283
284 @decorators.WithTimeoutAndRetriesFromInstance()
285 def GetBatteryInfo(self, timeout=None, retries=None):
286 """Gets battery info for the device.
287
288 Args:
289 timeout: timeout in seconds
290 retries: number of retries
291 Returns:
292 A dict containing various battery information as reported by dumpsys
293 battery.
294 """
295 result = {}
296 # Skip the first line, which is just a header.
297 for line in self._device.RunShellCommand(
298 ['dumpsys', 'battery'], check_return=True)[1:]:
299 # If usb charging has been disabled, an extra line of header exists.
300 if 'UPDATES STOPPED' in line:
301 logging.warning('Dumpsys battery not receiving updates. '
302 'Run dumpsys battery reset if this is in error.')
303 elif ':' not in line:
304 logging.warning('Unknown line found in dumpsys battery: "%s"', line)
305 else:
306 k, v = line.split(':', 1)
307 result[k.strip()] = v.strip()
308 return result
309
310 @decorators.WithTimeoutAndRetriesFromInstance()
311 def GetCharging(self, timeout=None, retries=None):
312 """Gets the charging state of the device.
313
314 Args:
315 timeout: timeout in seconds
316 retries: number of retries
317 Returns:
318 True if the device is charging, false otherwise.
319 """
320 battery_info = self.GetBatteryInfo()
321 for k in ('AC powered', 'USB powered', 'Wireless powered'):
322 if (k in battery_info and
323 battery_info[k].lower() in ('true', '1', 'yes')):
324 return True
325 return False
326
327 @decorators.WithTimeoutAndRetriesFromInstance()
328 def SetCharging(self, enabled, timeout=None, retries=None):
329 """Enables or disables charging on the device.
330
331 Args:
332 enabled: A boolean indicating whether charging should be enabled or
333 disabled.
334 timeout: timeout in seconds
335 retries: number of retries
336
337 Raises:
338 device_errors.CommandFailedError: If method of disabling charging cannot
339 be determined.
340 """
341 self._DiscoverDeviceProfile()
342 if not self._cache['profile']['enable_command']:
343 raise device_errors.CommandFailedError(
344 'Unable to find charging commands.')
345
346 if enabled:
347 command = self._cache['profile']['enable_command']
348 else:
349 command = self._cache['profile']['disable_command']
350
351 def set_and_verify_charging():
352 self._device.RunShellCommand(command, check_return=True)
353 return self.GetCharging() == enabled
354
355 timeout_retry.WaitFor(set_and_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 ChargeDeviceToLevel(self, level, wait_period=60):
436 """Enables charging and waits for device to be charged to given level.
437
438 Args:
439 level: level of charge to wait for.
440 wait_period: time in seconds to wait between checking.
441 """
442 self.SetCharging(True)
443
444 def device_charged():
445 battery_level = self.GetBatteryInfo().get('level')
446 if battery_level is None:
447 logging.warning('Unable to find current battery level.')
448 battery_level = 100
449 else:
450 logging.info('current battery level: %s', battery_level)
451 battery_level = int(battery_level)
452 return battery_level >= level
453
454 timeout_retry.WaitFor(device_charged, wait_period=wait_period)
455
456 def LetBatteryCoolToTemperature(self, target_temp, wait_period=60):
457 """Lets device sit to give battery time to cool down
458 Args:
459 temp: maximum temperature to allow in tenths of degrees c.
460 wait_period: time in seconds to wait between checking.
461 """
462 def cool_device():
463 temp = self.GetBatteryInfo().get('temperature')
464 if temp is None:
465 logging.warning('Unable to find current battery temperature.')
466 temp = 0
467 else:
468 logging.info('Current battery temperature: %s', temp)
469 return int(temp) <= target_temp
470 self.EnableBatteryUpdates()
471 logging.info('Waiting for the device to cool down to %s (0.1 C)',
472 target_temp)
473 timeout_retry.WaitFor(cool_device, wait_period=wait_period)
474
475 @decorators.WithTimeoutAndRetriesFromInstance()
476 def TieredSetCharging(self, enabled, timeout=None, retries=None):
477 """Enables or disables charging on the device.
478
479 Args:
480 enabled: A boolean indicating whether charging should be enabled or
481 disabled.
482 timeout: timeout in seconds
483 retries: number of retries
484 """
485 if self.GetCharging() == enabled:
486 logging.warning('Device charging already in expected state: %s', enabled)
487 return
488
489 if enabled:
490 try:
491 self.SetCharging(enabled)
492 except device_errors.CommandFailedError:
493 logging.info('Unable to enable charging via hardware.'
494 ' Falling back to software enabling.')
495 self.EnableBatteryUpdates()
496 else:
497 try:
498 self._ClearPowerData()
499 self.SetCharging(enabled)
500 except device_errors.CommandFailedError:
501 logging.info('Unable to disable charging via hardware.'
502 ' Falling back to software disabling.')
503 self.DisableBatteryUpdates()
504
505 @contextlib.contextmanager
506 def PowerMeasurement(self, timeout=None, retries=None):
507 """Context manager that enables battery power collection.
508
509 Once the with block is exited, charging is resumed. Will attempt to disable
510 charging at the hardware level, and if that fails will fall back to software
511 disabling of battery updates.
512
513 Only for devices L and higher.
514
515 Example usage:
516 with PowerMeasurement():
517 browser_actions()
518 get_power_data() # report usage within this block
519 after_measurements() # Anything that runs after power
520 # measurements are collected
521
522 Args:
523 timeout: timeout in seconds
524 retries: number of retries
525 """
526 try:
527 self.TieredSetCharging(False, timeout=timeout, retries=retries)
528 yield
529 finally:
530 self.TieredSetCharging(True, timeout=timeout, retries=retries)
531
532 def _ClearPowerData(self):
533 """Resets battery data and makes device appear like it is not
534 charging so that it will collect power data since last charge.
535
536 Returns:
537 True if power data cleared.
538 False if power data clearing is not supported (pre-L)
539
540 Raises:
541 device_errors.DeviceVersionError: If power clearing is supported,
542 but fails.
543 """
544 if (self._device.build_version_sdk <
545 constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP):
546 logging.warning('Dumpsys power data only available on 5.0 and above. '
547 'Cannot clear power data.')
548 return False
549
550 self._device.RunShellCommand(
551 ['dumpsys', 'battery', 'set', 'usb', '1'], check_return=True)
552 self._device.RunShellCommand(
553 ['dumpsys', 'battery', 'set', 'ac', '1'], check_return=True)
554 self._device.RunShellCommand(
555 ['dumpsys', 'batterystats', '--reset'], check_return=True)
556 battery_data = self._device.RunShellCommand(
557 ['dumpsys', 'batterystats', '--charged', '--checkin'],
558 check_return=True, large_output=True)
559 for line in battery_data:
560 l = line.split(',')
561 if (len(l) > _PWI_POWER_CONSUMPTION_INDEX and l[_ROW_TYPE_INDEX] == 'pwi'
562 and l[_PWI_POWER_CONSUMPTION_INDEX] != 0):
563 self._device.RunShellCommand(
564 ['dumpsys', 'battery', 'reset'], check_return=True)
565 raise device_errors.CommandFailedError(
566 'Non-zero pmi value found after reset.')
567 self._device.RunShellCommand(
568 ['dumpsys', 'battery', 'reset'], check_return=True)
569 return True
570
571 def _DiscoverDeviceProfile(self):
572 """Checks and caches device information.
573
574 Returns:
575 True if profile is found, false otherwise.
576 """
577
578 if 'profile' in self._cache:
579 return True
580 for profile in _DEVICE_PROFILES:
581 if self._device.product_model == profile['name']:
582 self._cache['profile'] = profile
583 return True
584 self._cache['profile'] = {
585 'name': None,
586 'witness_file': None,
587 'enable_command': None,
588 'disable_command': None,
589 'charge_counter': None,
590 'voltage': None,
591 'current': None,
592 }
593 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