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
23 _CONTROL_CHARGING_COMMANDS
= [
26 'witness_file': '/sys/module/pm8921_charger/parameters/disabled',
27 'enable_command': 'echo 0 > /sys/module/pm8921_charger/parameters/disabled',
29 'echo 1 > /sys/module/pm8921_charger/parameters/disabled',
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',
38 'echo 0x4A > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && '
39 'echo 1 > /sys/class/power_supply/usb/online'),
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.
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.
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.
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
76 default_retries: An integer containing the default number or times an
77 operation should be retried on failure if no explicit
81 TypeError: If it is not passed a DeviceUtils instance.
83 if not isinstance(device
, device_utils
.DeviceUtils
):
84 raise TypeError('Must be initialized with DeviceUtils object.')
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.
94 timeout: timeout in seconds
95 retries: number of retries
98 Dict of power data, keyed on package names.
106 dumpsys_output
= self
._device
.RunShellCommand(
107 ['dumpsys', 'batterystats', '-c'], check_return
=True)
108 csvreader
= csv
.reader(dumpsys_output
)
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
):
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.
138 package: Package to get power data on.
141 Dict of UID and power data.
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.
154 timeout: timeout in seconds
155 retries: number of retries
157 A dict containing various battery information as reported by dumpsys
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
)
171 k
, v
= line
.split(':', 1)
172 result
[k
.strip()] = v
.strip()
175 def GetCharging(self
, timeout
=None, retries
=None):
176 """Gets the charging state of the device.
179 timeout: timeout in seconds
180 retries: number of retries
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')):
191 def SetCharging(self
, enabled
, timeout
=None, retries
=None):
192 """Enables or disables charging on the device.
195 enabled: A boolean indicating whether charging should be enabled or
197 timeout: timeout in seconds
198 retries: number of retries
201 device_errors.CommandFailedError: If method of disabling charging cannot
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
210 raise device_errors
.CommandFailedError(
211 'Unable to find charging commands.')
214 command
= self
._cache
['charging_config']['enable_command']
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.
230 timeout: timeout in seconds
231 retries: number of retries
234 device_errors.CommandFailedError: When resetting batterystats fails to
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'],
249 for line
in battery_data
:
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'],
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.
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'],
272 self
._device
.RunShellCommand(['dumpsys', 'battery', 'reset'],
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.
286 with BatteryMeasurement():
288 get_power_data() # report usage within this block
289 after_measurements() # Anything that runs after power
290 # measurements are collected
293 timeout: timeout in seconds
294 retries: number of retries
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.')
303 self
.DisableBatteryUpdates(timeout
=timeout
, retries
=retries
)
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.
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.')
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
)