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.
7 # pylint: disable=unused-argument
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
27 'witness_file': '/sys/module/pm8921_charger/parameters/disabled',
29 'echo 0 > /sys/module/pm8921_charger/parameters/disabled && '
30 'dumpsys battery reset'),
32 'echo 1 > /sys/module/pm8921_charger/parameters/disabled && '
33 'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
34 'charge_counter': None,
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',
46 'echo 0x4A > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && '
47 'echo 1 > /sys/class/power_supply/usb/online && '
48 'dumpsys battery reset'),
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,
62 'echo 1 > /sys/class/power_supply/battery/charging_enabled && '
63 'dumpsys battery reset'),
65 'echo 0 > /sys/class/power_supply/battery/charging_enabled && '
66 'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
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',
76 'echo Disconnected > '
77 '/sys/bus/i2c/drivers/bq2419x/0-006b/input_cable_state && '
78 'dumpsys battery reset'),
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',
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',
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.
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.
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
117 class BatteryUtils(object):
119 def __init__(self
, device
, default_timeout
=_DEFAULT_TIMEOUT
,
120 default_retries
=_DEFAULT_RETRIES
):
121 """BatteryUtils constructor.
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
128 default_retries: An integer containing the default number or times an
129 operation should be retried on failure if no explicit
133 TypeError: If it is not passed a DeviceUtils instance.
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
142 @decorators.WithTimeoutAndRetriesFromInstance()
143 def SupportsFuelGauge(self
, timeout
=None, retries
=None):
144 """Detect if fuel gauge chip is present.
147 timeout: timeout in seconds
148 retries: number of retries
151 True if known fuel gauge files are present.
154 self
._DiscoverDeviceProfile
()
155 return (self
._cache
['profile']['enable_command'] != None
156 and self
._cache
['profile']['charge_counter'] != None)
158 @decorators.WithTimeoutAndRetriesFromInstance()
159 def GetFuelGaugeChargeCounter(self
, timeout
=None, retries
=None):
160 """Get value of charge_counter on fuel gauge chip.
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.
166 timeout: timeout in seconds
167 retries: number of retries
170 value of charge_counter for fuel gauge chip in units of nAh.
173 device_errors.CommandFailedError: If fuel gauge chip not found.
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.')
181 @decorators.WithTimeoutAndRetriesFromInstance()
182 def GetNetworkData(self
, package
, timeout
=None, retries
=None):
183 """Get network data for specific package.
186 package: package name you want network data for.
187 timeout: timeout in seconds
188 retries: number of retries
191 Tuple of (sent_data, recieved_data)
192 None if no network data found
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']:
199 if package
not in self
._cache
['uids']:
200 logging
.warning('No UID found for %s. Can\'t get network data.',
204 network_data_path
= '/proc/uid_stat/%s/' % self
._cache
['uids'][package
]
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
)
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
)
217 return (send_data
, recv_data
)
219 @decorators.WithTimeoutAndRetriesFromInstance()
220 def GetPowerData(self
, timeout
=None, retries
=None):
221 """Get power data for device.
224 timeout: timeout in seconds
225 retries: number of retries
228 Dict of power data, keyed on package names.
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
]))
264 return {p
: {'uid': uid
, 'data': pwi_entries
[uid
]}
265 for p
, uid
in self
._cache
['uids'].iteritems()}
267 @decorators.WithTimeoutAndRetriesFromInstance()
268 def GetPackagePowerData(self
, package
, timeout
=None, retries
=None):
269 """Get power data for particular package.
272 package: Package to get power data on.
275 Dict of UID and power data.
280 None if the package is not found in the power data.
282 return self
.GetPowerData().get(package
)
284 @decorators.WithTimeoutAndRetriesFromInstance()
285 def GetBatteryInfo(self
, timeout
=None, retries
=None):
286 """Gets battery info for the device.
289 timeout: timeout in seconds
290 retries: number of retries
292 A dict containing various battery information as reported by dumpsys
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
)
306 k
, v
= line
.split(':', 1)
307 result
[k
.strip()] = v
.strip()
310 @decorators.WithTimeoutAndRetriesFromInstance()
311 def GetCharging(self
, timeout
=None, retries
=None):
312 """Gets the charging state of the device.
315 timeout: timeout in seconds
316 retries: number of retries
318 True if the device is charging, false otherwise.
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')):
327 @decorators.WithTimeoutAndRetriesFromInstance()
328 def SetCharging(self
, enabled
, timeout
=None, retries
=None):
329 """Enables or disables charging on the device.
332 enabled: A boolean indicating whether charging should be enabled or
334 timeout: timeout in seconds
335 retries: number of retries
338 device_errors.CommandFailedError: If method of disabling charging cannot
341 self
._DiscoverDeviceProfile
()
342 if not self
._cache
['profile']['enable_command']:
343 raise device_errors
.CommandFailedError(
344 'Unable to find charging commands.')
347 command
= self
._cache
['profile']['enable_command']
349 command
= self
._cache
['profile']['disable_command']
351 def set_and_verify_charging():
352 self
._device
.RunShellCommand(command
, check_return
=True, as_root
=True)
353 return self
.GetCharging() == enabled
355 timeout_retry
.WaitFor(set_and_verify_charging
, wait_period
=1)
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.
364 timeout: timeout in seconds
365 retries: number of retries
368 device_errors.CommandFailedError: When resetting batterystats fails to
370 device_errors.DeviceVersionError: If device is not L or higher.
372 def battery_updates_disabled():
373 return self
.GetCharging() is False
375 self
._ClearPowerData
()
376 self
._device
.RunShellCommand(['dumpsys', 'battery', 'set', 'ac', '0'],
378 self
._device
.RunShellCommand(['dumpsys', 'battery', 'set', 'usb', '0'],
380 timeout_retry
.WaitFor(battery_updates_disabled
, wait_period
=1)
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.
388 timeout: timeout in seconds
389 retries: number of retries
392 device_errors.DeviceVersionError: If device is not L or higher.
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)))
399 self
._device
.RunShellCommand(['dumpsys', 'battery', 'reset'],
401 timeout_retry
.WaitFor(battery_updates_enabled
, wait_period
=1)
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.
410 Only for devices L and higher.
413 with BatteryMeasurement():
415 get_power_data() # report usage within this block
416 after_measurements() # Anything that runs after power
417 # measurements are collected
420 timeout: timeout in seconds
421 retries: number of retries
424 device_errors.DeviceVersionError: If device is not L or higher.
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.')
430 self
.DisableBatteryUpdates(timeout
=timeout
, retries
=retries
)
433 self
.EnableBatteryUpdates(timeout
=timeout
, retries
=retries
)
435 def ChargeDeviceToLevel(self
, level
, wait_period
=60):
436 """Enables charging and waits for device to be charged to given level.
439 level: level of charge to wait for.
440 wait_period: time in seconds to wait between checking.
442 self
.SetCharging(True)
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.')
450 logging
.info('current battery level: %s', battery_level
)
451 battery_level
= int(battery_level
)
452 return battery_level
>= level
454 timeout_retry
.WaitFor(device_charged
, wait_period
=wait_period
)
456 def LetBatteryCoolToTemperature(self
, target_temp
, wait_period
=60):
457 """Lets device sit to give battery time to cool down
459 temp: maximum temperature to allow in tenths of degrees c.
460 wait_period: time in seconds to wait between checking.
463 temp
= self
.GetBatteryInfo().get('temperature')
465 logging
.warning('Unable to find current battery temperature.')
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)',
473 timeout_retry
.WaitFor(cool_device
, wait_period
=wait_period
)
475 @decorators.WithTimeoutAndRetriesFromInstance()
476 def TieredSetCharging(self
, enabled
, timeout
=None, retries
=None):
477 """Enables or disables charging on the device.
480 enabled: A boolean indicating whether charging should be enabled or
482 timeout: timeout in seconds
483 retries: number of retries
485 if self
.GetCharging() == enabled
:
486 logging
.warning('Device charging already in expected state: %s', enabled
)
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()
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()
505 @contextlib.contextmanager
506 def PowerMeasurement(self
, timeout
=None, retries
=None):
507 """Context manager that enables battery power collection.
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.
513 Only for devices L and higher.
516 with PowerMeasurement():
518 get_power_data() # report usage within this block
519 after_measurements() # Anything that runs after power
520 # measurements are collected
523 timeout: timeout in seconds
524 retries: number of retries
527 self
.TieredSetCharging(False, timeout
=timeout
, retries
=retries
)
530 self
.TieredSetCharging(True, timeout
=timeout
, retries
=retries
)
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.
537 True if power data cleared.
538 False if power data clearing is not supported (pre-L)
541 device_errors.DeviceVersionError: If power clearing is supported,
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.')
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
:
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)
571 def _DiscoverDeviceProfile(self
):
572 """Checks and caches device information.
575 True if profile is found, false otherwise.
578 if 'profile' in self
._cache
:
580 for profile
in _DEVICE_PROFILES
:
581 if self
._device
.product_model
== profile
['name']:
582 self
._cache
['profile'] = profile
584 self
._cache
['profile'] = {
586 'witness_file': None,
587 'enable_command': None,
588 'disable_command': None,
589 'charge_counter': None,