1#!/usr/bin/env python3

2

3 import gi

4 import time

5 import re

6 import subprocess

7 import sys

8 import argparse

9

10 gi.require_version("Gio", "2.0")

11 from gi.repository import Gio # noqa: E402

12

13

14 class Battery:

15

16 def __init__(self, data):

17 lines = data.split("\n")

18 for line in lines:

19 if line.find("state:") != -1:

20 self._state = line.split(":")[1].strip()

21 elif line.find("energy:") != -1:

22 self._energy, self._energy_units = self._get_capacity(line)

23 elif line.find("energy-full:") != -1:

24 self._energy_full, self._energy_full_units = (

25 self._get_capacity(line)

26 )

27 elif line.find("energy-full-design:") != -1:

28 self._energy_full_design, self._energy_full_design_units = (

29 self._get_capacity(line)

30 )

31

32 def _get_capacity(self, line):

33 """

34 Given a line of input that represents a battery capacity (energy)

35 value, return a tuple of (value, units). Value is returned as a

36 float.

37 """

38 capacity = line.split(":")[1].strip()

39 values = capacity.split()

40 return (float(values[0]), values[1])

41

42 def __str__(self):

43 ret = "-----------------------------------------\n"

44 ret += "State: %s\n" % self._state

45 ret += "Energy: %s %s\n" % (self._energy, self._energy_units)

46 ret += "Energy Full: %s %s\n" % (

47 self._energy_full,

48 self._energy_full_units,

49 )

50 ret += "Energy Full-Design: %s %s\n" % (

51 self._energy_full_design,

52 self._energy_full_design_units,

53 )

54 return ret

55

56

57 def find_battery():

58 batinfo = subprocess.Popen(

59 "upower -d",

60 stdout=subprocess.PIPE,

61 shell=True,

62 universal_newlines=True,

63 )

64 if not batinfo:

65 return None

66 else:

67 out, err = batinfo.communicate()

68 if out:

69 device_regex = re.compile(r"Device: (.*battery_.*)")

70 batteries = device_regex.findall(out)

71 if len(batteries) == 0:

72 return None

73 elif len(batteries) > 1:

74 print(

75 "Warning: This system has more than 1 battery, only the"

76 "first battery will be measured"

77 )

78 return batteries[0]

79 else:

80 return None

81

82

83 def get_battery_state():

84 battery_name = find_battery()

85 if battery_name is None:

86 return None

87

88 batinfo = subprocess.Popen(

89 "upower -i %s" % battery_name,

90 stdout=subprocess.PIPE,

91 shell=True,

92 universal_newlines=True,

93 )

94 if not batinfo:

95 return None

96 else:

97 out, err = batinfo.communicate()

98 if out:

99 return Battery(out)

100 else:

101 return None

102

103

104 def validate_battery_info(battery):

105 if battery is None:

106 print("Error obtaining battery info")

107 return False

108 if battery._state != "discharging":

109 print("Error: battery is not discharging, test will not be valid")

110 return False

111 return True

112

113

114 def battery_life(before, after, time):

115 capacity_difference = before._energy - after._energy

116 print(

117 "Battery drained by %f %s"

118 % (capacity_difference, before._energy_units)

119 )

120 if capacity_difference == 0:

121 print(

122 "Battery capacity did not change, unable to determine remaining"

123 " time",

124 file=sys.stderr,

125 )

126 return 1

127 drain_per_second = capacity_difference / time

128 print(

129 "Battery drained %f %s per second"

130 % (drain_per_second, before._energy_units)

131 )

132

133 # the battery at it's max design capacity (when it was brand new)

134 design_life_minutes = round(

135 ((before._energy_full_design / drain_per_second) / 60), 2

136 )

137 print(

138 "Battery Life with full battery at design capacity (when new): %.2f"

139 "minutes" % (design_life_minutes)

140 )

141

142 # the battery at it's current max capacity

143 current_full_life_minutes = round(

144 ((before._energy_full / drain_per_second) / 60), 2

145 )

146 print(

147 "Battery Life with a full battery at current capacity: %.2f minutes"

148 % (current_full_life_minutes)

149 )

150

151 # the battery at it's current capacity

152 current_life_minutes = round(((before._energy / drain_per_second) / 60), 2)

153 print(

154 "Battery Life with at current battery capacity: %.2f minutes"

155 % (current_life_minutes)

156 )

157 return 0

158

159

160 def main():

161 parser = argparse.ArgumentParser(

162 description="""Determine battery drain and battery life by running

163 the specified action. Battery life is shown for:

164 current capacity, capacity when battery is full,

165 and capacity when battery is full and was brand new

166 (design capacity)"""

167 )

168 parser.add_argument(

169 "-i",

170 "--idle",

171 help="Run the test while system is" " idling",

172 action="store_true",

173 )

174 parser.add_argument(

175 "-s3",

176 "--sleep",

177 help="Run the test while system" " is suspended",

178 action="store_true",

179 )

180 parser.add_argument(

181 "-t",

182 "--time",

183 help="Specify the allotted time in seconds to run",

184 type=int,

185 required=True,

186 )

187 parser.add_argument(

188 "-m", "--movie", help="Run the test while playing the file MOVIE"

189 )

190 args = parser.parse_args()

191

192 test_time = args.time

193 battery_before = get_battery_state()

194 if not validate_battery_info(battery_before):

195 return 1

196 print(battery_before)

197

198 if args.idle:

199 time.sleep(test_time)

200 elif args.movie:

201 totem_settings = Gio.Settings.new("org.gnome.totem")

202 totem_settings.set_boolean("repeat", True)

203 a = subprocess.Popen(["totem", "--fullscreen", args.movie])

204 time.sleep(test_time)

205 a.kill()

206 totem_settings = Gio.Settings.new("org.gnome.totem")

207 totem_settings.set_boolean("repeat", False)

208 elif args.sleep:

209 subprocess.call(["fwts", "s3", "--s3-sleep-delay=" + str(test_time)])

210

211 battery_after = get_battery_state()

212 if not validate_battery_info(battery_after):

213 return 1

214 print(battery_after)

215

216 return battery_life(battery_before, battery_after, test_time)

217

218

219 if __name__ == "__main__":

220 sys.exit(main())