xref: /aosp_15_r20/external/autotest/site_utils/admin_audit/battery_validator.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1#!/usr/bin/env python3
2# Copyright 2020 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Functional to validate RPM configs in the lab."""
6
7from __future__ import absolute_import
8from __future__ import division
9from __future__ import print_function
10
11import os
12import logging
13
14import common
15from autotest_lib.site_utils.admin_audit import constants
16
17
18class BatteryValidator(object):
19    """Battery validator provides capacity verification of battery on the host.
20
21    The state detection and set state as:
22    - NORMAL - battery capacity >= 70%
23    - ACCEPTABLE - battery capacity >= 40%
24    - NEED_REPLACEMENT - battery capacity < 40%
25    - UNKNOWN - logic cannot read data to specify the state
26    - NOT_DETECTED - battery is not present on the host
27    """
28    # Battery capacity levels
29    BATTER_NORMAL_LEVEL = 70
30    BATTER_ACCEPTABLE_LEVEL = 40
31
32    # Attempts to try read battery data
33    READ_DATA_RETRY_COUNT = 3
34
35    def __init__(self, host):
36        """Initialize the battery validator.
37
38        @params host CrosHost instance.
39        """
40        self._host = host
41        self._battery_path = None
42        self.charge_full = 0
43        self.charge_full_design = 0
44
45    def _read_battery_path(self):
46        """Detect path to battery properties on the host."""
47        self._battery_path = None
48        info = self._host.get_power_supply_info()
49        if 'Battery' not in info:
50            logging.debug('Battery is not presented but expected!'
51                          ' In some cases it possible.')
52            return None
53        self._battery_path = info['Battery']['path']
54        logging.info('Battery path: %s', self._battery_path)
55        return self._battery_path
56
57    def is_battery_expected(self):
58        """Verify if battery expected on the host based on host info."""
59        host_info = self._host.host_info_store.get()
60        return host_info.get_label_value('power') == 'battery'
61
62    def _read_data_from_host(self):
63        """Read data from the host."""
64
65        def read_val(file_name, field_type):
66            """Read a value from file."""
67            try:
68                path = os.path.join(self._battery_path, file_name)
69                out = self._host.run('cat %s' % path,
70                                     ignore_status=True).stdout.strip()
71                return field_type(out)
72            except:
73                return field_type(0)
74
75        self.charge_full = read_val('charge_full', float)
76        self.charge_full_design = read_val('charge_full_design', float)
77        cycle_count = read_val('cycle_count', int)
78        logging.debug('Battery cycle_count: %d', cycle_count)
79
80    def _validate_by_host(self):
81        """Validate battery by reading data from the host."""
82        logging.debug('Try to validate from host side.')
83        if self._host.is_up():
84            for _ in range(self.READ_DATA_RETRY_COUNT):
85                try:
86                    self._read_battery_path()
87                    if not self._battery_path:
88                        logging.info('Battery is not present/found on host')
89                        return self._update_host_info(
90                                constants.HW_STATE_NOT_DETECTED)
91                    self._read_data_from_host()
92                    return self._update_battery_state()
93                except Exception as e:
94                    logging.debug('(Not critical) %s', e)
95        return None
96
97    def _validate_by_servo(self):
98        """Validate battery by servo access."""
99        servo = self._host.servo
100        logging.debug('Try to validate from servo side.')
101        if servo:
102            for _ in range(self.READ_DATA_RETRY_COUNT):
103                try:
104                    if not servo.has_control('battery_full_charge_mah'):
105                        break
106                    self.charge_full = servo.get('battery_full_charge_mah')
107                    self.charge_full_design = servo.get(
108                            'battery_full_design_mah')
109                    return self._update_battery_state()
110                except Exception as e:
111                    logging.debug('(Not critical) %s', e)
112        return None
113
114    def validate(self):
115        """Validate battery and update state.
116
117        Try to validate from host if device is sshable if not then try
118        read battery info by servo.
119        """
120        logging.info('Starting battery validation.')
121        state = None
122        if not self.is_battery_expected():
123            state = self._update_host_info(constants.HW_STATE_NOT_DETECTED)
124        if not state:
125            state = self._validate_by_host()
126        if not state:
127            state = self._validate_by_servo()
128        if not state:
129            state = self._update_host_info(constants.HW_STATE_UNKNOWN)
130        return state
131
132    def _update_battery_state(self):
133        """Update battery state based on batter charging capacity
134
135        The logic will update state based on:
136            if capacity >= 70% then NORMAL
137            if capacity >= 40% then ACCEPTABLE
138            if capacity  < 40% then NEED_REPLACEMENT
139        """
140        if self.charge_full == 0:
141            logging.debug('charge_full is 0. Skip update battery_state!')
142            return
143        if self.charge_full_design == 0:
144            logging.debug('charge_full_design is 0.'
145                          ' Skip update battery_state!')
146            return
147        capacity = (100.0 * self.charge_full / self.charge_full_design)
148        logging.debug('Battery capacity: %d', capacity)
149
150        if capacity >= self.BATTER_NORMAL_LEVEL:
151            return self._update_host_info(constants.HW_STATE_NORMAL)
152        if capacity >= self.BATTER_ACCEPTABLE_LEVEL:
153            return self._update_host_info(constants.HW_STATE_ACCEPTABLE)
154        return self._update_host_info(constants.HW_STATE_NEED_REPLACEMENT)
155
156    def _update_host_info(self, state):
157        """Update state value to the battery_state in the host_info
158
159        @param state: new state value for the label
160        """
161        if self._host:
162            state_prefix = constants.BATTERY_STATE_PREFIX
163            host_info = self._host.host_info_store.get()
164            old_state = host_info.get_label_value(state_prefix)
165            host_info.set_version_label(state_prefix, state)
166            logging.info('Set %s as `%s` (previous: `%s`)', state_prefix,
167                         state, old_state)
168            self._host.host_info_store.commit(host_info)
169        return state
170