OLD | NEW |
---|---|
(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 _CONTROL_CHARGING_COMMANDS = [ | |
24 { | |
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', | |
30 }, | |
31 { | |
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'), | |
44 }, | |
45 ] | |
46 | |
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 | |
63 | |
64 | |
65 class BatteryUtils(object): | |
66 | |
67 def __init__(self, device, default_timeout=_DEFAULT_TIMEOUT, | |
68 default_retries=_DEFAULT_RETRIES): | |
69 """BatteryUtils constructor. | |
70 | |
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. | |
79 | |
80 Raises: | |
81 TypeError: If it is not passed a DeviceUtils instance. | |
82 """ | |
83 self._device = device | |
84 if not isinstance(device, device_utils.DeviceUtils): | |
85 raise TypeError('Must be initialized with device utils object.') | |
jbudorick
2015/03/27 13:26:35
s/device utils/DeviceUtils/
rnephew (Wrong account)
2015/03/27 22:17:02
Done.
| |
86 self._default_timeout = default_timeout | |
87 self._default_retries = default_retries | |
88 | |
89 @decorators.WithTimeoutAndRetriesFromInstance() | |
90 def GetPowerData(self, timeout=None, retries=None): | |
91 """ Get power data for device. | |
92 Args: | |
93 timeout: timeout in seconds | |
94 retries: number of retries | |
95 | |
96 Returns: | |
97 Dict of power data, keyed on package names. | |
98 { package_name: { | |
jbudorick
2015/03/27 13:26:35
nit: formatting. Drop the key onto its own line.
rnephew (Wrong account)
2015/03/27 22:17:02
Done.
| |
99 'uid': uid, | |
100 'data': [1,2,3] | |
101 }, | |
102 } | |
103 """ | |
104 dumpsys_output = self._device.RunShellCommand( | |
105 ['dumpsys', 'batterystats', '-c'], check_return=True) | |
106 csvreader = csv.reader(dumpsys_output) | |
107 uid_entries = {} | |
108 pwi_entries = collections.defaultdict(list) | |
109 for entry in csvreader: | |
110 if entry[_DUMP_VERSION_INDEX] not in ['8', '9']: | |
111 # Wrong dumpsys version. | |
jbudorick
2015/03/27 13:26:35
This should _at least_ be logged. Perhaps it shoul
rnephew (Wrong account)
2015/03/27 22:17:01
Done.
| |
112 break | |
113 if _ROW_TYPE_INDEX >= len(entry): | |
114 continue | |
115 if entry[_ROW_TYPE_INDEX] == 'uid': | |
116 current_package = entry[_PACKAGE_NAME_INDEX] | |
117 assert current_package not in uid_entries | |
jbudorick
2015/03/27 13:26:35
Don't assert on data we're getting from the dumpsy
rnephew (Wrong account)
2015/03/27 22:17:02
Done.
| |
118 uid_entries[current_package] = entry[_PACKAGE_UID_INDEX] | |
119 elif (_PWI_POWER_CONSUMPTION_INDEX < len(entry) and | |
jbudorick
2015/03/27 13:26:36
'and' should start the next line rather than end t
rnephew (Wrong account)
2015/03/27 22:17:01
Done.
| |
120 entry[_ROW_TYPE_INDEX] == 'pwi' and | |
121 entry[_PWI_AGGREGATION_INDEX] == 'l'): | |
122 pwi_entries[entry[_PWI_UID_INDEX]].append( | |
123 float(entry[_PWI_POWER_CONSUMPTION_INDEX])) | |
124 | |
125 out_dict = {} | |
jbudorick
2015/03/27 13:26:36
Why are we building this separately? Can we just b
rnephew (Wrong account)
2015/03/27 22:17:01
Talked about this offline, cliff notes of the reas
| |
126 for p in uid_entries: | |
127 out_dict[p] = {} | |
128 out_dict[p]['uid'] = uid_entries[p] | |
129 out_dict[p]['data'] = pwi_entries[uid_entries[p]] | |
130 return out_dict | |
131 | |
132 def GetPackagePowerData(self, package, timeout=None, retries=None): | |
133 """ Get power data for particular package. | |
134 | |
135 Args: | |
136 package: Package to get power data on. | |
137 | |
138 returns: | |
139 Dict of UID and power data. | |
140 { 'uid': uid, | |
jbudorick
2015/03/27 13:26:36
Same.
rnephew (Wrong account)
2015/03/27 22:17:01
Done.
| |
141 'data': [1,2,3] | |
142 } | |
143 """ | |
144 return self.GetPowerData()[package] | |
jbudorick
2015/03/27 13:26:35
This should gracefully handle |package| not being
rnephew (Wrong account)
2015/03/27 22:17:01
Done.
| |
145 | |
146 @decorators.WithTimeoutAndRetriesFromInstance() | |
147 def GetBatteryInfo(self, timeout=None, retries=None): | |
148 """Gets battery info for the device. | |
149 | |
150 Args: | |
151 timeout: timeout in seconds | |
152 retries: number of retries | |
153 Returns: | |
154 A dict containing various battery information as reported by dumpsys | |
155 battery. | |
156 """ | |
157 result = {} | |
158 # Skip the first line, which is just a header. | |
159 for line in self._device.RunShellCommand( | |
160 ['dumpsys', 'battery'], check_return=True)[1:]: | |
161 # If usb charging has been disabled, an extra line of header exists. | |
162 if 'UPDATES STOPPED' in line: | |
163 logging.warning('Dumpsys battery not receiving updates. ' | |
164 'Run dumpsys battery reset if this is in error.') | |
165 elif ':' not in line: | |
166 logging.warning('Unknown line found in dumpsys battery.') | |
167 logging.warning(line) | |
168 else: | |
169 k, v = line.split(': ', 1) | |
170 result[k.strip()] = v.strip() | |
171 return result | |
172 | |
173 @decorators.WithTimeoutAndRetriesFromInstance() | |
174 def GetCharging(self, timeout=None, retries=None): | |
175 """Gets the charging state of the device. | |
176 | |
177 Args: | |
178 timeout: timeout in seconds | |
179 retries: number of retries | |
180 Returns: | |
181 True if the device is charging, false otherwise. | |
182 """ | |
183 battery_info = self.GetBatteryInfo() | |
184 for k in ('AC powered', 'USB powered', 'Wireless powered'): | |
185 if (k in battery_info and | |
186 battery_info[k].lower() in ('true', '1', 'yes')): | |
187 return True | |
188 return False | |
189 | |
190 @decorators.WithTimeoutAndRetriesFromInstance() | |
191 def SetCharging(self, enabled, timeout=None, retries=None): | |
192 """Enables or disables charging on the device. | |
193 | |
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 | |
199 | |
200 Raises: | |
201 device_errors.CommandFailedError: If method of disabling charging cannot | |
202 be determined. | |
203 """ | |
204 charging_config = self._device.GetCacheEntry('charging_config') | |
205 if not charging_config: | |
206 for c in _CONTROL_CHARGING_COMMANDS: | |
207 if self._device.FileExists(c['witness_file']): | |
208 charging_config = c | |
209 self._device.SetCacheEntry('charging_config', c) | |
210 break | |
211 else: | |
212 raise device_errors.CommandFailedError( | |
213 'Unable to find charging commands.') | |
214 | |
215 if enabled: | |
216 command = charging_config['enable_command'] | |
217 else: | |
218 command = charging_config['disable_command'] | |
219 | |
220 def set_and_verify_charging(): | |
221 self._device.RunShellCommand(command, check_return=True) | |
222 return self.GetCharging() == enabled | |
223 | |
224 timeout_retry.WaitFor(set_and_verify_charging, wait_period=1) | |
225 | |
226 # TODO(rnephew): Make private when all use cases can use the context manager. | |
227 @decorators.WithTimeoutAndRetriesFromInstance() | |
228 def DisableBatteryUpdates(self, timeout=None, retries=None): | |
229 """ Resets battery data and makes device appear like it is not | |
230 charging so that it will collect power data since last charge. | |
231 | |
232 Args: | |
233 timeout: timeout in seconds | |
234 retries: number of retries | |
235 | |
236 Raises: | |
237 device_errors.CommandFailedError: When resetting batterystats fails to | |
238 reset power values. | |
239 """ | |
240 def battery_updates_disabled(): | |
241 return self.GetCharging() is False | |
242 | |
243 self._device.RunShellCommand( | |
244 ['dumpsys', 'battery', 'set', 'usb', '1']) | |
jbudorick
2015/03/27 13:26:35
check_return=True
rnephew (Wrong account)
2015/03/27 22:17:02
Done.
| |
245 self._device.RunShellCommand( | |
246 ['dumpsys', 'batterystats', '--reset'], check_return=True) | |
247 battery_data = self._device.RunShellCommand( | |
248 ['dumpsys', 'batterystats', '--charged', '--checkin'], | |
249 check_return=True) | |
250 for line in battery_data: | |
251 l = line.split(',') | |
252 if (len(l) > _PWI_POWER_CONSUMPTION_INDEX and l[_ROW_TYPE_INDEX] == 'pwi' | |
253 and l[_PWI_POWER_CONSUMPTION_INDEX] != 0): | |
254 raise device_errors.CommandFailedError( | |
255 'Non-zero pmi value found after reset.') | |
256 self._device.RunShellCommand(['dumpsys', 'battery', 'set', 'usb', '0'], | |
257 check_return=True) | |
jbudorick
2015/03/27 13:26:35
Nit: formatting.
Here, I'd drop the command onto
rnephew (Wrong account)
2015/03/27 22:17:01
Done.
| |
258 timeout_retry.WaitFor(battery_updates_disabled, wait_period=1) | |
259 | |
260 # TODO(rnephew): Make private when all use cases can use the context manager. | |
261 @decorators.WithTimeoutAndRetriesFromInstance() | |
262 def EnableBatteryUpdates(self, timeout=None, retries=None): | |
263 """ Restarts device charging so that dumpsys no longer collects power data. | |
264 | |
265 Args: | |
266 timeout: timeout in seconds | |
267 retries: number of retries | |
268 """ | |
269 def battery_updates_enabled(): | |
270 return self.GetCharging() is True | |
271 | |
272 self._device.RunShellCommand(['dumpsys', 'battery', 'set', 'usb', '1'], | |
273 check_return=True) | |
jbudorick
2015/03/27 13:26:35
Same formatting nit.
rnephew (Wrong account)
2015/03/27 22:17:02
Done.
| |
274 self._device.RunShellCommand(['dumpsys', 'battery', 'reset'], | |
275 check_return=True) | |
276 timeout_retry.WaitFor(battery_updates_enabled, wait_period=1) | |
277 | |
278 @contextlib.contextmanager | |
279 def BatteryMeasurement(self, timeout=None, retries=None): | |
280 """Context manager that enables battery data collection. It makes | |
281 the device appear to stop charging so that dumpsys will start collecting | |
282 power data since last charge. Once the with block is exited, charging is | |
283 resumed and power data since last charge is no longer collected. | |
284 | |
285 Only for devices L and higher. | |
286 | |
287 Example usage: | |
288 with BatteryMeasurement(): | |
289 browser_actions() | |
290 get_power_data() # report usage within this block | |
jbudorick
2015/03/27 13:26:35
What will this typically look like?
Should Batter
rnephew (Wrong account)
2015/03/27 22:17:02
My thoughts against that are, you could run GetPow
| |
291 after_measurements() # Anything that runs after power | |
292 # measurements are collected | |
293 | |
294 Args: | |
295 timeout: timeout in seconds | |
296 retries: number of retries | |
297 | |
298 Raises: | |
299 device_errors.CommandFailedError: If device is not L or higher. | |
300 """ | |
301 if (self._device.build_version_sdk < | |
302 constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP): | |
303 raise device_errors.CommandFailedError('Device must be L or higher.') | |
304 try: | |
305 self.DisableBatteryUpdates(timeout=timeout, retries=retries) | |
306 yield | |
307 finally: | |
308 self.EnableBatteryUpdates(timeout=timeout, retries=retries) | |
309 | |
OLD | NEW |