Path: blob/master/AndroidRunner/Plugins/batterymanager/Batterymanager.py
907 views
import csv1import numpy as np2import os.path as op3import os4import pandas as pd5import time6import re78from AndroidRunner.Plugins.Profiler import Profiler91011class Batterymanager(Profiler):1213ANDROID_VERSION_11_API_LEVEL_30 = 3014BATTERYMANAGER_DEVICE_OUTPUT_FILE = '/storage/emulated/0/Documents/BatteryManager.csv'15AVAILABLE_DATA_POINTS = ['ACTION_CHARGING', 'ACTION_DISCHARGING',16'BATTERY_HEALTH_COLD', 'BATTERY_HEALTH_DEAD', 'BATTERY_HEALTH_GOOD',17'BATTERY_HEALTH_OVERHEAT',18'BATTERY_HEALTH_OVER_VOLTAGE', 'BATTERY_HEALTH_UNKNOWN',19'BATTERY_HEALTH_UNSPECIFIED_FAILURE',20'BATTERY_PLUGGED_AC', 'BATTERY_PLUGGED_DOCK', 'BATTERY_PLUGGED_USB',21'BATTERY_PLUGGED_WIRELESS',22'BATTERY_PROPERTY_CAPACITY', 'BATTERY_PROPERTY_CHARGE_COUNTER',23'BATTERY_PROPERTY_CURRENT_AVERAGE',24'BATTERY_PROPERTY_CURRENT_NOW', 'BATTERY_PROPERTY_ENERGY_COUNTER',25'BATTERY_PROPERTY_STATUS',26'BATTERY_STATUS_CHARGING', 'BATTERY_STATUS_DISCHARGING', 'BATTERY_STATUS_FULL',27'BATTERY_STATUS_NOT_CHARGING', 'BATTERY_STATUS_UNKNOWN',28'EXTRA_BATTERY_LOW', 'EXTRA_HEALTH', 'EXTRA_ICON_SMALL', 'EXTRA_LEVEL', 'EXTRA_PLUGGED',29'EXTRA_PRESENT',30'EXTRA_SCALE', 'EXTRA_STATUS', 'EXTRA_TECHNOLOGY', 'EXTRA_TEMPERATURE', 'EXTRA_VOLTAGE']3132AVAILABLE_PERSISTENCY_STRATEGIES = ['csv', 'adb_log']3334def __init__(self, config, paths):35super(Batterymanager, self).__init__(config, paths)36self.output_dir = ''37self.paths = paths38self.profile = False3940self.sampling_rate = config.get('sample_interval', 1000) # default: every second4142self.data_points = self.validate_config('data_points',43config['data_points'],44Batterymanager.AVAILABLE_DATA_POINTS)4546self.persistency_strategy = self.validate_config('persistency_strategy',47config['persistency_strategy'],48Batterymanager.AVAILABLE_PERSISTENCY_STRATEGIES)4950def validate_config(self, field, raw_data_points, available_data_points):51invalid_data_points = [52dp for dp in raw_data_points if dp not in set(available_data_points)]53if invalid_data_points:54self.logger.warning(55'Invalid {} in config: {}'.format(field, invalid_data_points))56return [dp for dp in raw_data_points57if dp in available_data_points]5859# Check if the selected data points are valid60def start_profiling(self, device, **kwargs):61device.shell(self.build_intent(True))6263def stop_profiling(self, device, **kwargs):64device.shell(self.build_intent(False))6566def build_intent(self, is_start):67if is_start:68intent_data_fields = ','.join(self.data_points)69intent_to_csv = 'true' if 'csv' in self.persistency_strategy else 'false'70intent = f'am start-foreground-service -n "com.example.batterymanager_utility/com.example' \71f'.batterymanager_utility.DataCollectionService" --ei sampleRate {self.sampling_rate} --es ' \72f'"dataFields" "{intent_data_fields}" --ez toCSV {intent_to_csv}'73else:74intent = f'am stopservice com.example.batterymanager_utility/com.example.batterymanager_utility' \75f'.DataCollectionService'7677return intent7879def collect_results(self, device):80# sleep for 5 seconds to make sure the service has stopped81time.sleep(5)82if 'csv' in self.persistency_strategy:83batterymanager_csv_file = op.join(self.output_dir,84'{}_{}.csv'.format(device.id, time.strftime('%Y.%m.%d_%H%M%S')))85device.pull('{}'.format(self.BATTERYMANAGER_DEVICE_OUTPUT_FILE), batterymanager_csv_file)86device.shell('rm -f {}'.format(self.BATTERYMANAGER_DEVICE_OUTPUT_FILE))8788if 'adb_log' in self.persistency_strategy:89logcat_file = op.join(self.output_dir,90'logcat_{}_{}.txt'.format(device.id, time.strftime('%Y.%m.%d_%H%M%S')))91self.pull_logcat(device, logcat_file)9293header, rows = self.get_logcat(device)94self.write_logcat_csv(device, header, rows)9596@staticmethod97def pull_logcat(device, logcat_file):98"""99From Android 11 (API level 30) the path /mnt/sdcard cannot be accessed via ADB100as you don't have permissions to access this path. However, we can access /sdcard.101"""102device_api_version = int(device.shell("getprop ro.build.version.sdk"))103104if device_api_version >= Batterymanager.ANDROID_VERSION_11_API_LEVEL_30:105logcat_output_file_device_dir_path = "/sdcard"106else:107logcat_output_file_device_dir_path = "/mnt/sdcard"108109device.shell(f"logcat -f {logcat_output_file_device_dir_path}/logcat.txt -d")110device.pull(f"{logcat_output_file_device_dir_path}/logcat.txt", logcat_file)111device.shell(f"rm -f {logcat_output_file_device_dir_path}/logcat.txt")112113@staticmethod114def get_logcat(device):115header_pattern = 'BatteryMgr:DataCollectionService: onStartCommand: rawFields => '116data_pattern = 'BatteryMgr:DataCollectionService: stats => '117118raw_header = device.logcat_regex(header_pattern)119raw_rows = device.logcat_regex(data_pattern)120121return raw_header, raw_rows122123@staticmethod124def preprocess_logcat(header, rows):125header = header.split('=> ')[1]126header = header.split('\n')[0]127header = header.split(',')128129# FOR OLDER DEVICES remove all non-letter characters except _130header = [re.sub(r'[^a-zA-Z_]', '', h) for h in header]131132rows = rows.split('\n')133# FOR OLDER DEVICES remove rows containing "134rows = [row for row in rows if '"' not in row]135rows = [row.split('=> ')[1].split(',') for row in rows]136137rows.sort(key=lambda x: x[0])138return header, rows139140def write_logcat_csv(self, device, header, rows):141header, rows = Batterymanager.preprocess_logcat(header, rows)142logcat_csv_file = op.join(self.output_dir,143'logcat_{}_{}.csv'.format(device.id, time.strftime('%Y.%m.%d_%H%M%S')))144145with open(logcat_csv_file, 'w') as lc_csv_file:146csv_writer = csv.writer(lc_csv_file)147csv_writer.writerow(header)148for row in rows:149csv_writer.writerow(row)150151def unload(self, device):152return153154def dependencies(self):155return ['com.example.batterymanager_utility']156157def load(self, device):158return159160def set_output(self, output_dir):161self.output_dir = output_dir162163def aggregate_subject(self):164return165166@staticmethod167def list_subdir(a_dir):168"""List immediate subdirectories of a_dir"""169# https://stackoverflow.com/a/800201170return [name for name in os.listdir(a_dir)171if os.path.isdir(os.path.join(a_dir, name))]172173@staticmethod174def preprocess_values(df):175df['Timestamp'] = df['Timestamp'] - df['Timestamp'][0]176# conversion from microseconds to seconds177df['Timestamp'] = df['Timestamp'] / 1000178return df179180@staticmethod181def calculate_power(df):182df['power'] = (abs(df['BATTERY_PROPERTY_CURRENT_NOW']) / 1000 / 1000) * (df['EXTRA_VOLTAGE'] / 1000)183return df184185@staticmethod186def trapezoid_method(df):187return np.trapz(df['power'].values, df['Timestamp'].values)188189@staticmethod190def aggregate_batterymanager_runs(logs_dir):191runs = pd.DataFrame()192run_number = 0193for run_file in [f for f in os.listdir(logs_dir) if os.path.isfile(os.path.join(logs_dir, f))]:194f_name = os.path.join(logs_dir, run_file)195if not f_name.endswith(".csv"):196continue197run_df = pd.read_csv(f_name)198199stats = {}200if 'BATTERY_PROPERTY_CURRENT_NOW' in run_df.columns and 'EXTRA_VOLTAGE' in run_df.columns:201run_df = Batterymanager.preprocess_values(run_df)202run_df = Batterymanager.calculate_power(run_df)203avg_power = run_df['power'].mean()204stats.update({'Avg power (W)': avg_power})205stats.update({'Energy simple (J)': avg_power * run_df['Timestamp'].max()})206stats.update({'Energy trapz (J)': Batterymanager.trapezoid_method(run_df)})207stats.update(run_df.mean().to_dict())208stats.update({'run': run_number})209run_number += 1210211runs = pd.concat([runs, pd.DataFrame(stats, index=[0])], ignore_index=True)212213runs = runs.drop(columns=['Timestamp', 'power'], axis=1)214return runs215216@staticmethod217def aggregate(data_dir):218df = pd.DataFrame()219for device in Batterymanager.list_subdir(data_dir):220device_dir = os.path.join(data_dir, device)221for subject in Batterymanager.list_subdir(device_dir):222subject_dir = os.path.join(device_dir, subject)223if os.path.isdir(os.path.join(subject_dir, 'batterymanager')):224runs_df = Batterymanager.aggregate_batterymanager_runs(os.path.join(subject_dir, 'batterymanager'))225runs_df['subject'] = subject226runs_df['device'] = device227df = pd.concat([df, runs_df], ignore_index=True)228return df[df.columns[::-1]]229230def aggregate_end(self, data_dir, output_file):231print(('Output file: {}'.format(output_file)))232rows = self.aggregate(data_dir)233rows.to_csv(output_file, index=False)234235236