autotest: Log battery info for analysis

BUG=b:163572001
TEST=run local repair

Change-Id: I351010bd51b3e1d53dba8e22787605b0eefe26a7
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/autotest/+/2434491
Tested-by: Otabek Kasimov <otabek@google.com>
Reviewed-by: Garry Wang <xianuowang@chromium.org>
Commit-Queue: Otabek Kasimov <otabek@google.com>
diff --git a/server/hosts/cros_repair.py b/server/hosts/cros_repair.py
index 372d0bd..e649a68 100644
--- a/server/hosts/cros_repair.py
+++ b/server/hosts/cros_repair.py
@@ -10,6 +10,7 @@
 import json
 import logging
 import time
+import math
 
 import common
 from autotest_lib.client.common_lib import error
@@ -100,6 +101,9 @@
 
     # Battery discharging state in power_supply_info file.
     BATTERY_DISCHARGING = 'Discharging'
+    # Power controller can discharge battery any time till 90% for any model.
+    # Setting level to 85% in case we have wearout of it.
+    BATTERY_DISCHARGE_MIN = 85
 
     @timeout_util.TimeoutDecorator(cros_constants.VERIFY_TIMEOUT_SEC)
     def verify(self, host):
@@ -129,7 +133,21 @@
     def _validate_battery(self, host, info):
         try:
             charging_state = info['Battery']['state']
-            if charging_state == self.BATTERY_DISCHARGING:
+            battery_level = float(info['Battery']['percentage'])
+
+            # Collect info to determine which battery level is better to call
+            # as MIN_BATTERY_LEVEL for DUTs in the lab.
+            battery_level_by_10 = int(math.floor(battery_level / 10.0)) * 10
+            metrics_data = {
+                    'model': host.host_info_store.get().model,
+                    'level': battery_level_by_10,
+                    'mode': charging_state
+            }
+            metrics.Counter('chromeos/autotest/battery/state').increment(
+                    fields=metrics_data)
+
+            if (charging_state == self.BATTERY_DISCHARGING
+                        and battery_level < self.BATTERY_DISCHARGE_MIN):
                 logging.debug('Try to fix discharging state of the battery. '
                               'Possible that a test left wrong state.')
                 # Here is the chance that battery is discharging because
@@ -137,19 +155,20 @@
                 # We are going to try to fix it by set charging to normal.
                 host.run('ectool chargecontrol normal', ignore_status=True)
                 # wait to change state.
-                time.sleep(5)
+                time.sleep(10)
                 info = self._load_info(host)
                 charging_state = info['Battery']['state']
                 fixed = charging_state != self.BATTERY_DISCHARGING
                 # TODO (@otabek) remove metrics after research
-                metrics_data = {'host': host.hostname,
-                                'model': host.host_info_store.get().model,
-                                'fixed': fixed}
+                logging.debug('Fixed battery discharge mode.')
+                metrics_data = {
+                        'model': host.host_info_store.get().model,
+                        'fixed': fixed
+                }
                 metrics.Counter(
                     'chromeos/autotest/repair/chargecontrol_fixed'
                 ).increment(fields=metrics_data)
 
-            battery_level = float(info['Battery']['percentage'])
             if (battery_level < MIN_BATTERY_LEVEL and
                 charging_state == self.BATTERY_DISCHARGING):
                 # TODO(@xianuowang) remove metrics here once we have device
diff --git a/server/hosts/servo_repair.py b/server/hosts/servo_repair.py
index 1de4b4c..cb794a6 100644
--- a/server/hosts/servo_repair.py
+++ b/server/hosts/servo_repair.py
@@ -10,6 +10,7 @@
 import sys
 import functools
 import logging
+import math
 import time
 
 import common
@@ -583,6 +584,63 @@
         return 'pwr_button control is normal'
 
 
+class _BatteryVerifier(hosts.Verifier):
+    """Collect battery info for analysis."""
+
+    @ignore_exception_for_non_cros_host
+    @timeout_util.TimeoutDecorator(cros_constants.VERIFY_TIMEOUT_SEC)
+    def verify(self, host):
+        try:
+            servo = host.get_servo()
+            charging = False
+            if servo.has_control('battery_is_charging'):
+                charging = servo.get('battery_is_charging')
+            level = -1
+            if servo.has_control('battery_charge_percent'):
+                level = servo.get('battery_charge_percent')
+            design_mah = servo.get('battery_full_design_mah')
+            charge_mah = servo.get('battery_full_charge_mah')
+            logging.info('Charging: %s', charging)
+            logging.info('Percentage: %s', level)
+            logging.info('Full charge max: %s', charge_mah)
+            logging.info('Full design max: %s', design_mah)
+            # based on analysis of ratio we can find out what is
+            # the level when we can say that battery is dead
+            ratio = int(math.floor(charge_mah / design_mah * 100.0))
+            logging.info('Ratio: %s', ratio)
+            data = {
+                    'board': host.servo_board or 'unknown',
+                    'model': host.servo_model or 'unknown',
+                    'ratio': ratio
+            }
+            metrics.Counter('chromeos/autotest/battery/ratio').increment(
+                    fields=data)
+        except Exception as e:
+            # Keeping it with info level because we do not expect it.
+            logging.info('(Not critical) %s', e)
+
+    def _is_applicable(self, host):
+        if not host.is_ec_supported():
+            logging.info('The board not support EC')
+            return False
+        dut_info = host.get_dut_host_info()
+        if dut_info:
+            host_info = host.get_dut_host_info()
+            if host_info.get_label_value('power') != 'battery':
+                logging.info('The board does not have battery')
+                return False
+        servo = host.get_servo()
+        if (not servo.has_control('battery_full_design_mah')
+                    or not servo.has_control('battery_full_charge_mah')):
+            logging.info('The board is not supported battery controls...')
+            return False
+        return True
+
+    @property
+    def description(self):
+        return 'Logs battery levels'
+
+
 class _LidVerifier(hosts.Verifier):
     """
     Verifier to check sanity of the `lid_open` signal.
@@ -832,6 +890,7 @@
             (_ServodControlVerifier, 'servod_control', ['servod_connection']),
             (_DUTConnectionVerifier, 'dut_connected', ['servod_connection']),
             (_PowerButtonVerifier, 'pwr_button', ['dut_connected']),
+            (_BatteryVerifier, 'battery', ['dut_connected']),
             (_LidVerifier, 'lid_open', ['dut_connected']),
             (_EcBoardVerifier, 'ec_board', ['dut_connected']),
             (_CCDTestlabVerifier, 'ccd_testlab', ['dut_connected']),