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