1# Copyright (c) 2011 The Chromium OS 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
5import logging, math, time
6
7from autotest_lib.client.bin import test
8from autotest_lib.client.common_lib import error
9from autotest_lib.client.cros import rtc
10from autotest_lib.client.cros.power import power_dashboard
11from autotest_lib.client.cros.power import power_status
12from autotest_lib.client.cros.power import power_telemetry_utils
13from autotest_lib.client.cros.power import power_suspend
14from autotest_lib.client.cros.power import power_utils
15
16
17class power_Standby(test.test):
18    """Measure Standby power test."""
19    version = 1
20    _percent_min_charge = 10
21    _min_sample_hours = 0.1
22
23    def initialize(self, pdash_note=''):
24        """Reset force discharge state."""
25        self._force_discharge_enabled = False
26        self._pdash_note = pdash_note
27        self._checkpoint_logger = power_status.CheckpointLogger()
28
29    def run_once(self, test_hours=None, sample_hours=None,
30                 max_milliwatts_standby=500, ac_ok=False,
31                 force_discharge=False, suspend_state='', bypass_check=False):
32        """Put DUT to suspend state for |sample_hours| and measure power."""
33        if not power_utils.has_battery():
34            raise error.TestNAError('Skipping test because DUT has no battery.')
35
36        if test_hours < sample_hours:
37            raise error.TestFail('Test hours must be greater than sample '
38                                 'hours.')
39
40        # If we're measuring < 6min of standby then the S0 time is not
41        # negligible. Note, reasonable rule of thumb is S0 idle is ~10-20 times
42        # standby power.
43        if sample_hours < self._min_sample_hours and not bypass_check:
44            raise error.TestFail('Must standby more than %.2f hours.' % \
45                                 sample_hours)
46
47        power_stats = power_status.get_status()
48
49        if not ac_ok and power_stats.on_ac():
50            raise error.TestError('On AC, please unplug power supply.')
51
52        if force_discharge:
53            if not power_stats.on_ac():
54                raise error.TestError('Not on AC, please plug in power supply '
55                                      'to attempt force discharge.')
56            if not power_utils.charge_control_by_ectool(False):
57                raise error.TestError('Unable to force discharge.')
58
59            self._force_discharge_enabled = True
60
61        charge_start = power_stats.battery[0].charge_now
62        voltage_start = power_stats.battery[0].voltage_now
63
64        max_hours = ((charge_start * voltage_start) /
65                     (max_milliwatts_standby / 1000.))
66        if max_hours < test_hours:
67            raise error.TestFail('Battery not charged adequately for test.')
68
69        suspender = power_suspend.Suspender(self.resultsdir,
70                                            suspend_state=suspend_state)
71
72        elapsed_hours = 0
73
74        results = {}
75        loop = 0
76        start_ts = time.time()
77
78        while elapsed_hours < test_hours:
79            charge_before = power_stats.battery[0].charge_now
80            before_suspend_secs = rtc.get_seconds()
81            suspender.suspend(duration=sample_hours * 3600)
82            after_suspend_secs = rtc.get_seconds()
83
84            power_stats.refresh()
85            if power_stats.percent_current_charge() < self._percent_min_charge:
86                logging.warning('Battery = %.2f%%.  Too low to continue.')
87                break
88
89            # check that the RTC slept the correct amount of time as there could
90            # potentially be another wake source that would spoil the test.
91            actual_hours = (after_suspend_secs - before_suspend_secs) / 3600.0
92            percent_diff = math.fabs((actual_hours - sample_hours) / (
93                    (actual_hours + sample_hours) / 2) * 100)
94            if percent_diff > 2 and not bypass_check:
95                err = 'Requested standby time and actual varied by %.2f%%.' \
96                    % percent_diff
97                raise error.TestFail(err)
98
99            # Check resulting charge consumption
100            charge_used = charge_before - power_stats.battery[0].charge_now
101            elapsed_hours += actual_hours
102            logging.debug(
103                    'loop%d done: loop hours %.3f, elapsed hours %.3f '
104                    'charge used: %.3f', loop, actual_hours, elapsed_hours,
105                    charge_used)
106            loop += 1
107
108        end_ts = time.time()
109        offset = (end_ts - start_ts - elapsed_hours * 3600) / 2.
110        offset += suspender.get_suspend_delay()
111        start_ts += offset
112        end_ts -= offset
113        power_telemetry_utils.start_measurement(start_ts)
114        power_telemetry_utils.end_measurement(end_ts)
115        self._checkpoint_logger.checkpoint(self.tagged_testname,
116                                           start_ts, end_ts)
117        charge_end = power_stats.battery[0].charge_now
118        total_charge_used = charge_start - charge_end
119        if total_charge_used <= 0 and not bypass_check:
120            raise error.TestError('Charge used is suspect.')
121
122        voltage_end = power_stats.battery[0].voltage_now
123        standby_hours = power_stats.battery[0].charge_full_design / \
124                        total_charge_used * elapsed_hours
125        energy_used = (voltage_start + voltage_end) / 2 * \
126                      total_charge_used
127
128        results['ah_charge_start'] = charge_start
129        results['ah_charge_now'] = charge_end
130        results['ah_charge_used'] = total_charge_used
131        results['force_discharge'] = self._force_discharge_enabled
132        results['hours_standby_time'] = standby_hours
133        results['hours_standby_time_tested'] = elapsed_hours
134        results['v_voltage_start'] = voltage_start
135        results['v_voltage_now'] = voltage_end
136        results['w_energy_rate'] = energy_used / elapsed_hours
137        results['wh_energy_used'] = energy_used
138
139        self.write_perf_keyval(results)
140        pdash = power_dashboard.SimplePowerLoggerDashboard(
141                end_ts - start_ts, results['w_energy_rate'],
142                self.tagged_testname, start_ts, self.resultsdir,
143                note=self._pdash_note)
144        pdash.upload()
145        self._checkpoint_logger.save_checkpoint_data(self.resultsdir)
146
147        self.output_perf_value(description='hours_standby_time',
148                               value=results['hours_standby_time'],
149                               units='hours', higher_is_better=True)
150        self.output_perf_value(description='w_energy_rate',
151                               value=results['w_energy_rate'], units='watts',
152                               higher_is_better=False)
153
154        # need to sleep for some time to allow network connection to return
155        time.sleep(10)
156
157    def cleanup(self):
158        """Clean up force discharge."""
159        if self._force_discharge_enabled:
160            power_utils.charge_control_by_ectool(True)
161