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