Adding Peter Thatcher to the owners file.
[chromium-blink-merge.git] / build / android / pylib / device / battery_utils.py
blobcca89c725e6e1608b1d674ebfa319709af5ca6fa
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.
5 """Provides a variety of device interactions with power.
6 """
7 # pylint: disable=unused-argument
9 import collections
10 import contextlib
11 import csv
12 import logging
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
20 _DEFAULT_TIMEOUT = 30
21 _DEFAULT_RETRIES = 3
23 _CONTROL_CHARGING_COMMANDS = [
25 # Nexus 4
26 'witness_file': '/sys/module/pm8921_charger/parameters/disabled',
27 'enable_command': 'echo 0 > /sys/module/pm8921_charger/parameters/disabled',
28 'disable_command':
29 'echo 1 > /sys/module/pm8921_charger/parameters/disabled',
32 # Nexus 5
33 # Setting the HIZ bit of the bq24192 causes the charger to actually ignore
34 # energy coming from USB. Setting the power_supply offline just updates the
35 # Android system to reflect that.
36 'witness_file': '/sys/kernel/debug/bq24192/INPUT_SRC_CONT',
37 'enable_command': (
38 'echo 0x4A > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && '
39 'echo 1 > /sys/class/power_supply/usb/online'),
40 'disable_command': (
41 'echo 0xCA > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && '
42 'chmod 644 /sys/class/power_supply/usb/online && '
43 'echo 0 > /sys/class/power_supply/usb/online'),
47 # The list of useful dumpsys columns.
48 # Index of the column containing the format version.
49 _DUMP_VERSION_INDEX = 0
50 # Index of the column containing the type of the row.
51 _ROW_TYPE_INDEX = 3
52 # Index of the column containing the uid.
53 _PACKAGE_UID_INDEX = 4
54 # Index of the column containing the application package.
55 _PACKAGE_NAME_INDEX = 5
56 # The column containing the uid of the power data.
57 _PWI_UID_INDEX = 1
58 # The column containing the type of consumption. Only consumtion since last
59 # charge are of interest here.
60 _PWI_AGGREGATION_INDEX = 2
61 # The column containing the amount of power used, in mah.
62 _PWI_POWER_CONSUMPTION_INDEX = 5
65 class BatteryUtils(object):
67 def __init__(self, device, default_timeout=_DEFAULT_TIMEOUT,
68 default_retries=_DEFAULT_RETRIES):
69 """BatteryUtils constructor.
71 Args:
72 device: A DeviceUtils instance.
73 default_timeout: An integer containing the default number of seconds to
74 wait for an operation to complete if no explicit value
75 is provided.
76 default_retries: An integer containing the default number or times an
77 operation should be retried on failure if no explicit
78 value is provided.
80 Raises:
81 TypeError: If it is not passed a DeviceUtils instance.
82 """
83 if not isinstance(device, device_utils.DeviceUtils):
84 raise TypeError('Must be initialized with DeviceUtils object.')
85 self._device = device
86 self._cache = device.GetClientCache(self.__class__.__name__)
87 self._default_timeout = default_timeout
88 self._default_retries = default_retries
90 @decorators.WithTimeoutAndRetriesFromInstance()
91 def GetPowerData(self, timeout=None, retries=None):
92 """ Get power data for device.
93 Args:
94 timeout: timeout in seconds
95 retries: number of retries
97 Returns:
98 Dict of power data, keyed on package names.
100 package_name: {
101 'uid': uid,
102 'data': [1,2,3]
106 dumpsys_output = self._device.RunShellCommand(
107 ['dumpsys', 'batterystats', '-c'], check_return=True)
108 csvreader = csv.reader(dumpsys_output)
109 uid_entries = {}
110 pwi_entries = collections.defaultdict(list)
111 for entry in csvreader:
112 if entry[_DUMP_VERSION_INDEX] not in ['8', '9']:
113 # Wrong dumpsys version.
114 raise device_errors.DeviceVersionError(
115 'Dumpsys version must be 8 or 9. %s found.'
116 % entry[_DUMP_VERSION_INDEX])
117 if _ROW_TYPE_INDEX >= len(entry):
118 continue
119 if entry[_ROW_TYPE_INDEX] == 'uid':
120 current_package = entry[_PACKAGE_NAME_INDEX]
121 if current_package in uid_entries:
122 raise device_errors.CommandFailedError(
123 'Package %s found multiple times' % (current_package))
124 uid_entries[current_package] = entry[_PACKAGE_UID_INDEX]
125 elif (_PWI_POWER_CONSUMPTION_INDEX < len(entry)
126 and entry[_ROW_TYPE_INDEX] == 'pwi'
127 and entry[_PWI_AGGREGATION_INDEX] == 'l'):
128 pwi_entries[entry[_PWI_UID_INDEX]].append(
129 float(entry[_PWI_POWER_CONSUMPTION_INDEX]))
131 return {p: {'uid': uid, 'data': pwi_entries[uid]}
132 for p, uid in uid_entries.iteritems()}
134 def GetPackagePowerData(self, package, timeout=None, retries=None):
135 """ Get power data for particular package.
137 Args:
138 package: Package to get power data on.
140 returns:
141 Dict of UID and power data.
143 'uid': uid,
144 'data': [1,2,3]
146 None if the package is not found in the power data.
148 return self.GetPowerData().get(package)
150 def GetBatteryInfo(self, timeout=None, retries=None):
151 """Gets battery info for the device.
153 Args:
154 timeout: timeout in seconds
155 retries: number of retries
156 Returns:
157 A dict containing various battery information as reported by dumpsys
158 battery.
160 result = {}
161 # Skip the first line, which is just a header.
162 for line in self._device.RunShellCommand(
163 ['dumpsys', 'battery'], check_return=True)[1:]:
164 # If usb charging has been disabled, an extra line of header exists.
165 if 'UPDATES STOPPED' in line:
166 logging.warning('Dumpsys battery not receiving updates. '
167 'Run dumpsys battery reset if this is in error.')
168 elif ':' not in line:
169 logging.warning('Unknown line found in dumpsys battery: "%s"', line)
170 else:
171 k, v = line.split(':', 1)
172 result[k.strip()] = v.strip()
173 return result
175 def GetCharging(self, timeout=None, retries=None):
176 """Gets the charging state of the device.
178 Args:
179 timeout: timeout in seconds
180 retries: number of retries
181 Returns:
182 True if the device is charging, false otherwise.
184 battery_info = self.GetBatteryInfo()
185 for k in ('AC powered', 'USB powered', 'Wireless powered'):
186 if (k in battery_info and
187 battery_info[k].lower() in ('true', '1', 'yes')):
188 return True
189 return False
191 def SetCharging(self, enabled, timeout=None, retries=None):
192 """Enables or disables charging on the device.
194 Args:
195 enabled: A boolean indicating whether charging should be enabled or
196 disabled.
197 timeout: timeout in seconds
198 retries: number of retries
200 Raises:
201 device_errors.CommandFailedError: If method of disabling charging cannot
202 be determined.
204 if 'charging_config' not in self._cache:
205 for c in _CONTROL_CHARGING_COMMANDS:
206 if self._device.FileExists(c['witness_file']):
207 self._cache['charging_config'] = c
208 break
209 else:
210 raise device_errors.CommandFailedError(
211 'Unable to find charging commands.')
213 if enabled:
214 command = self._cache['charging_config']['enable_command']
215 else:
216 command = self._cache['charging_config']['disable_command']
218 def set_and_verify_charging():
219 self._device.RunShellCommand(command, check_return=True)
220 return self.GetCharging() == enabled
222 timeout_retry.WaitFor(set_and_verify_charging, wait_period=1)
224 # TODO(rnephew): Make private when all use cases can use the context manager.
225 def DisableBatteryUpdates(self, timeout=None, retries=None):
226 """ Resets battery data and makes device appear like it is not
227 charging so that it will collect power data since last charge.
229 Args:
230 timeout: timeout in seconds
231 retries: number of retries
233 Raises:
234 device_errors.CommandFailedError: When resetting batterystats fails to
235 reset power values.
237 def battery_updates_disabled():
238 return self.GetCharging() is False
240 self._device.RunShellCommand(
241 ['dumpsys', 'battery', 'set', 'usb', '1'], check_return=True)
242 self._device.RunShellCommand(
243 ['dumpsys', 'batterystats', '--reset'], check_return=True)
244 battery_data = self._device.RunShellCommand(
245 ['dumpsys', 'batterystats', '--charged', '--checkin'],
246 check_return=True)
247 ROW_TYPE_INDEX = 3
248 PWI_POWER_INDEX = 5
249 for line in battery_data:
250 l = line.split(',')
251 if (len(l) > PWI_POWER_INDEX and l[ROW_TYPE_INDEX] == 'pwi'
252 and l[PWI_POWER_INDEX] != 0):
253 raise device_errors.CommandFailedError(
254 'Non-zero pmi value found after reset.')
255 self._device.RunShellCommand(['dumpsys', 'battery', 'set', 'usb', '0'],
256 check_return=True)
257 timeout_retry.WaitFor(battery_updates_disabled, wait_period=1)
259 # TODO(rnephew): Make private when all use cases can use the context manager.
260 def EnableBatteryUpdates(self, timeout=None, retries=None):
261 """ Restarts device charging so that dumpsys no longer collects power data.
263 Args:
264 timeout: timeout in seconds
265 retries: number of retries
267 def battery_updates_enabled():
268 return self.GetCharging() is True
270 self._device.RunShellCommand(['dumpsys', 'battery', 'set', 'usb', '1'],
271 check_return=True)
272 self._device.RunShellCommand(['dumpsys', 'battery', 'reset'],
273 check_return=True)
274 timeout_retry.WaitFor(battery_updates_enabled, wait_period=1)
276 @contextlib.contextmanager
277 def BatteryMeasurement(self, timeout=None, retries=None):
278 """Context manager that enables battery data collection. It makes
279 the device appear to stop charging so that dumpsys will start collecting
280 power data since last charge. Once the with block is exited, charging is
281 resumed and power data since last charge is no longer collected.
283 Only for devices L and higher.
285 Example usage:
286 with BatteryMeasurement():
287 browser_actions()
288 get_power_data() # report usage within this block
289 after_measurements() # Anything that runs after power
290 # measurements are collected
292 Args:
293 timeout: timeout in seconds
294 retries: number of retries
296 Raises:
297 device_errors.CommandFailedError: If device is not L or higher.
299 if (self._device.build_version_sdk <
300 constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP):
301 raise device_errors.DeviceVersionError('Device must be L or higher.')
302 try:
303 self.DisableBatteryUpdates(timeout=timeout, retries=retries)
304 yield
305 finally:
306 self.EnableBatteryUpdates(timeout=timeout, retries=retries)
308 def ChargeDeviceToLevel(self, level, wait_period=60):
309 """Enables charging and waits for device to be charged to given level.
311 Args:
312 level: level of charge to wait for.
313 wait_period: time in seconds to wait between checking.
315 self.SetCharging(True)
317 def device_charged():
318 battery_level = self.GetBatteryInfo().get('level')
319 if battery_level is None:
320 logging.warning('Unable to find current battery level.')
321 battery_level = 100
322 else:
323 logging.info('current battery level: %s', battery_level)
324 battery_level = int(battery_level)
325 return battery_level >= level
327 timeout_retry.WaitFor(device_charged, wait_period=wait_period)