Battery Monitoring System
- class src.pico_power_management.battery_monitor.BatteryMonitor(capacity_ah, max_voltage, min_voltage, critical_voltage, critical_percentage, charge_efficiency=1, discharge_efficiency=1)
Battery Montioring Class, incorporating methods for determining the state of charge.
The battery monitor must be changed to best fit the chemistry, voltage, and capacity of the attached battery.
The battery monitoring algorithims are derived from the following article for State of Charge (SoC) https://www.analog.com/en/technical-articles/a-closer-look-at-state-of-charge-and-state-health-estimation-tech.html
- BATTERY_STATES = {'CRITICAL': 2, 'ENERGY_SAVER': 1, 'PERFORMANCE': 0}
- initial_state_of_charge(battery_dict)
Determines the initial state of charge either from memory or estimation using battery voltage observed.
- Parameters
battery_dict (dict) – Dictionary of battery values from the poll_adc_channels() method.
- monitor_battery(battery_dict)
Method for monitoring a batteries state of charge using ADC readings and Coulomb counting.
- Parameters
battery_dict (dict) – Dictionary of battery values from the poll_adc_channels() method.
- Returns
State of charge percentage, battery health percentage, and power state.
- Return type
tuple
- state_of_charge(current, time_interval_ms)
Determines the State of Battery Charge using Coulomb Counting. Requires a recent current reading from the battery and a time interval between measurements.
- Parameters
current (float) – Current being supplied to/from the battery. Written in Amps. Charging is negative, Supplying is positive.
time_interval_ms (int | float) – Time interval between measurements in milliseconds
Code
1# -----------------------------------------
2# NOTES
3# -----------------------------------------
4
5# Dieter Steinhauser
6# 11/2023
7
8# The Battery we are using is a WEIZE 12V 50Ah LiFePO4, 4 Cells
9# Max/Full Charge 14.6V, 3.65V/cell
10# Nominal Voltage = 12.8V, 3.2V/cell
11# Full Discharge = 10V, 2.50V/Cell
12# LiFePO4 Voltage Chart, Best estimated while loaded and not charging.
13
14
15# 100% Charging - 3.65V/Cell - 14.6V
16# 100% Rest - 3.40/Cell - 13.6V
17# 90% - 3.35/Cell - 13.4V
18# 80% - 3.32/Cell - 13.3V
19# 70% - 3.30/Cell - 13.2V
20# 60% - 3.27/Cell - 13.1V
21# 50% - 3.26/Cell - 13.0V
22# 40% - 3.25/Cell - 13.0V
23# 30% - 3.22/Cell - 12.9V
24# 20% - 3.20/Cell - 12.8V
25# 10% - 3.00/Cell - 12.0V
26# 0% - 2.50/Cell - 10.0V
27
28# STATES
29
30# Battery Full - MPPT Charging - no load # Waste State -> Performance State
31# Battery Full - MPPT Charging - load # Performance State
32# Battery Full - No MPPT - no load # Energy Saving State, minor tasks
33# Battery Full - No MPPT - load # Performance State, Minor tasks
34
35
36# Battery mid - MPPT Charging - no load # Energy Saving State, Recharging
37# Battery mid - MPPT Charging - load # Energy Saving State, Recharging / minor tasks
38# Battery mid - No MPPT - no load # Energy Saving State, Depletion
39# Battery mid - No MPPT - load # Unwanted State, Depletion
40
41# Battery low - MPPT Charging - no load # Recovery State, Critical Recharging
42# Battery low - MPPT Charging - load # Recovery State, Critical Recharging
43# Battery low - no MPPT - no load # Recovery State, Critical Depletion
44# Battery low - no MPPT - load # Recovery State, Critical Depletion
45
46# -----------------------------------------
47# IMPORTS
48# -----------------------------------------
49
50import time
51
52# -----------------------------------------
53# METHODS
54# -----------------------------------------
55
56
57class BatteryMonitor:
58 """
59 Battery Montioring Class, incorporating methods for determining the state of charge.
60
61 The battery monitor must be changed to best fit the chemistry, voltage, and capacity of the
62 attached battery.
63
64 The battery monitoring algorithims are derived from the following article for State of Charge (SoC)
65 https://www.analog.com/en/technical-articles/a-closer-look-at-state-of-charge-and-state-health-estimation-tech.html
66
67 """
68
69 BATTERY_STATES = {'PERFORMANCE': 0, 'ENERGY_SAVER': 1, 'CRITICAL': 2}
70
71 def __init__ (self, capacity_ah, max_voltage, min_voltage, critical_voltage, critical_percentage, charge_efficiency=1, discharge_efficiency=1):
72 """
73 Initialization of the battery monitoring system.
74
75 :param capacity_ah: Battery Capacity in Amp Hours
76 :type capacity_ah: int | float
77 :param max_voltage: Maximum Voltage possible for the battery
78 :type max_voltage: int | float
79 :param min_voltage: Minimum Voltage possible for the battery
80 :type min_voltage: int | float
81 :param critical_voltage: Voltage that is deemed the critical point for low power mode.
82 :type critical_voltage: int | float
83 :param critical_percentage: Percentage that is deemed the critical point for low power mode.
84 :type critical_percentage: int | float
85 :param charge_efficiency: Efficiency of battery charging, defaults to 1
86 :type charge_efficiency: int | float, optional
87 :param discharge_efficiency: Efficiency of battery discharging, defaults to 1
88 :type discharge_efficiency: int | float, optional
89 """
90
91 self.capacity_ah = capacity_ah # battery Capacity in amp hours
92 self.capacity_mah = 1000 * capacity_ah # battery Capacity in milliamp hours
93 self.critical_voltage = critical_voltage # Voltage theshold for low battery in volts
94 self.critical_percentage = critical_percentage # charge percentage threshold for low battery.
95 self.max_voltage = max_voltage # Maximum voltage of the battery when charging.
96 self.min_voltage = min_voltage # minimum voltage of the battery when discharging.
97 self.charge_percentage = 100
98 self.discharge_percentage = 100 - self.charge_percentage
99 self.health = 100
100 self.depth_of_charge = 0
101 self.depth_of_discharge = 0
102 self.charge_cycles = 0
103 self.discharge_efficiency = discharge_efficiency
104 self.charge_efficiency = charge_efficiency
105 self.time = time.ticks_ms()
106 self.peak_charge_time = time.time()
107 self.state = self.BATTERY_STATES.get('ENERGY_SAVER')
108
109 def initial_state_of_charge(self, battery_dict):
110 """
111 Determines the initial state of charge either from memory or estimation using battery voltage observed.
112
113 :param battery_dict: Dictionary of battery values from the poll_adc_channels() method.
114 :type battery_dict: dict
115 """
116
117 # default to the last battery SOC saved to memory
118 # TODO: Read memory and determine the last saved state of charge or recall data from the Beelink via UART
119
120 # if none are available, try to estimate the SOC using voltage.
121
122 # parse the battery dictionary
123 battery_current = battery_dict.get("IBAT")
124 battery_voltage = battery_dict.get("VBAT")
125 charge_current = battery_dict.get("ICHARGER")
126
127 # estimate the initial state of charge based on the battery voltage
128 # calculated linear regression line of the Discharge chart. 95% confidence, Rsquared = 0.6705 for moderate correlation, voltage = 0.02577(charge) + 11.51.
129 charge_percentage = max(0, min(100, ((battery_voltage - 11.51) / 0.02577) ))
130
131 # edit the class variables.
132 self.charge_percentage = charge_percentage
133 self.discharge_percentage = 100 - self.charge_percentage
134 self.time = time.ticks_ms()
135
136
137
138 def state_of_charge(self, current, time_interval_ms):
139 """
140 Determines the State of Battery Charge using Coulomb Counting. Requires a recent current reading
141 from the battery and a time interval between measurements.
142
143 :param current: Current being supplied to/from the battery. Written in Amps. Charging is negative, Supplying is positive.
144 :type current: float
145 :param time_interval_ms: Time interval between measurements in milliseconds
146 :type time_interval_ms: int | float
147 """
148 # find the change in charge with respect to time.
149 # print(time_interval_ms)
150 delta_ah = current * time_interval_ms / 3_600_000
151 # print(f'delta Ah: {delta_ah}')
152
153 # find the percentage change in charge
154 percent_delta = 100 * (delta_ah / self.capacity_ah)
155 # print(f'percent_delta: {percent_delta}')
156
157 # Determine difference between charging and discharging efficiency.
158 efficiency = self.discharge_efficiency if current > 0 else self.charge_efficiency
159
160 # TODO: Determine if providing a minimum and maximum charging range here makes sense for DOD.
161 # print(f'discharge_percentage: {self.discharge_percentage}')
162 self.discharge_percentage = max(0, min(100, self.discharge_percentage + (efficiency * percent_delta)))
163 # self.discharge_percentage = self.discharge_percentage + (efficiency * percent_delta)
164 # print(f'discharge_percentage: {self.discharge_percentage}')
165
166 # Calculate the State of charge percentage based on the battery health and the discharge percentage.
167 self.charge_percentage = max(0, min(100, (self.health - self.discharge_percentage)))
168
169
170 def monitor_battery(self, battery_dict):
171 """
172 Method for monitoring a batteries state of charge using ADC readings and Coulomb counting.
173
174 :param battery_dict: Dictionary of battery values from the poll_adc_channels() method.
175 :type battery_dict: dict
176 :return: State of charge percentage, battery health percentage, and power state.
177 :rtype: tuple
178 """
179
180 # parse the battery dictionary
181 battery_current = battery_dict.get("IBAT")
182 battery_voltage = battery_dict.get("VBAT")
183 charge_current = battery_dict.get("ICHARGER")
184
185 # print(f'battery current: {battery_current}')
186 # print(f'Charger current: {charge_current}')
187 # #print(charge_current)
188
189 # find the time since the last time the battery monitored function was called.
190 new_time = time.ticks_ms()
191 time_elapsed = time.ticks_diff(new_time, self.time)
192 self.time = new_time
193 # scalar being multiplied to reflect the 0.25V:1A ratio
194 battery_current = ((battery_current - 2.5) * 4)
195 charge_current = max(0, charge_current * 4)
196
197 # print(f'battery current adjusted: {battery_current}')
198 # print(f'Charger current adjusted: {charge_current}')
199 # print(f'time elapsed: {time_elapsed}')
200
201 # If the battery is charging
202 if battery_current < 0:
203
204 # TODO Measure the smallest accepted current by the battery and change the charge current threshold accordingly.
205 # if the battery is fully charged, with max voltage achieved and charge current less than 50mA
206 if (battery_voltage >= self.max_voltage) and (-0.050 < battery_current < 0):
207
208 # if it has been more than 5 minutes since the last peak charge trigger
209 if self.peak_charge_time == 0 or ((time.time() - self.peak_charge_time) >= 300):
210
211 # Increment the amount of charge cycles that have occured
212 self.charge_cycles += 1
213
214 # if this is the first time that we have fully charged the battery, calibrate the SoC system.
215 if self.charge_cycles == 1:
216
217 # The Battery is fully charged, calibrate the system to reflect
218 self.discharge_percentage = 0
219 self.charge_percentage = 100
220
221 # Set the battery health to the state of charge
222 self.health = self.charge_percentage
223
224 else:
225 # calculate the state of charge and depth of discharge
226 self.state_of_charge(battery_current, time_elapsed)
227
228 # if the battery is at or above the healthy top of the charge or sees large charging current, allow for performance mode.
229 if(self.charge_percentage >= self.health) or (self.charge_percentage > 70 and charge_current > 2):
230 self.state = self.BATTERY_STATES.get('PERFORMANCE')
231
232 # if the battery is discharging
233 else:
234
235 # if the battery is lower than the discharged battery voltage
236 if battery_voltage <= self.min_voltage:
237
238 # if it has been more than 5 minutes since the last peak charge trigger
239 if self.peak_charge_time == 0 or ((time.time() - self.peak_charge_time) >= 300):
240
241 # Increment the amount of charge cycles that have occured
242 self.charge_cycles += 1
243
244 # if this is the first time that we have fully charged the battery, calibrate the SoC system.
245 if self.charge_cycles == 1:
246
247 # The Battery is fully discharged, calibrate the system to reflect this.
248 self.discharge_percentage = 100
249 self.charge_percentage = 0
250
251
252 # Set the battery health to the depth of discharge
253 self.health = self.discharge_percentage
254
255 else:
256 # calculate the state of charge and depth of discharge
257 self.state_of_charge(battery_current, time_elapsed)
258
259
260 # Handle states of low/critical power.
261 if (battery_voltage <= self.critical_voltage) or (self.charge_percentage <= self.critical_percentage):
262 self.state = self.BATTERY_STATES.get('CRITICAL')
263
264 else:
265 self.state = self.BATTERY_STATES.get('ENERGY_SAVER')
266
267 # write significant data to the flash.
268 # TODO Impelement Flash reading and writing or recalling data from the Beelink via UART
269
270 # return battery data for UART transmission.
271 bat_monitor_dict = {}
272 bat_monitor_dict['CHARGE'] = self.charge_percentage
273 bat_monitor_dict['HEALTH'] = self.health
274 bat_monitor_dict['STATE'] = self.state
275
276 return bat_monitor_dict
277
278if __name__ == '__main__':
279
280 bm = BatteryMonitor(capacity_ah = 50,
281 max_voltage = 14.4,
282 min_voltage = 10.2,
283 critical_voltage = 12.8,
284 critical_percentage= 20,
285 charge_efficiency=1,
286 discharge_efficiency=1 )
287
288
289
290
291# -----------------------------------------
292# END OF FILE
293# -----------------------------------------
294