Skip to content

Commit

Permalink
Calculation of the SOC based on coloumb-counting (#868)
Browse files Browse the repository at this point in the history
* Calculation of the SOC in the driver based on coloumb-counting

* soc_calc: add current correction before integration

* soc_calc: correction map for current

* Soc_calc: CorrectionMap, switch to turn on/off correction, selectable initial value

* soc_calc: Bugfix

* soc_calc: Bugfix

* store soc in dbus for restart

* store soc in dbus for restart (formatted)

* store soc in dbus for restart (bugfix)

* save soc_calc only after change > 1.0

* store soc in dbus for restart (bugfix)

* logger does not work this way. do not know why

* writing and reading to dbus works

* Removed options: SOC_CALC_CURRENT_CORRECTION, SOC_CALC_RESET_VALUE_ON_RESTART, SOC_CALC_INIT_VALUE
sort soc_calc alphabetically

* fixed comments
  • Loading branch information
cflenker committed Dec 23, 2023
1 parent 06dff6d commit 1500b1b
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 19 deletions.
95 changes: 80 additions & 15 deletions etc/dbus-serialbattery/battery.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ def init_values(self):
self.production = None
self.protection = Protection()
self.version = None
self.soc_calc_capacity_remain: float = None
self.soc_calc_capacity_remain_lasttime: float = None
self.soc_calc_reset_starttime: int = None
self.soc_calc: float = None # save soc_calc to preserve on restart
self.soc: float = None
self.time_to_soc_update: int = 0
self.charge_fet: bool = None
Expand Down Expand Up @@ -230,6 +234,11 @@ def manage_charge_voltage(self) -> None:
manages the charge voltage by setting self.control_voltage
:return: None
"""
if utils.SOC_CALCULATION:
self.soc_calculation()
else:
self.soc_calc = self.soc

self.prepare_voltage_management()
if utils.CVCM_ENABLE:
if utils.LINEAR_LIMITATION_ENABLE:
Expand All @@ -241,6 +250,60 @@ def manage_charge_voltage(self) -> None:
self.control_voltage = round(self.max_battery_voltage, 3)
self.charge_mode = "Keep always max voltage"

def soc_calculation(self) -> None:
current_time = time()
voltageSum = 0
current_corr = 0

for i in range(self.cell_count):
voltage = self.get_cell_voltage(i)
if voltage:
voltageSum += voltage

if self.soc_calc_capacity_remain:
current_corr = utils.calcLinearRelationship(
self.current,
utils.SOC_CALC_CURRENT_MEASURED,
utils.SOC_CALC_CURRENT_REAL,
)

self.soc_calc_capacity_remain = (
self.soc_calc_capacity_remain
+ current_corr
* (current_time - self.soc_calc_capacity_remain_lasttime)
/ 3600
)
self.soc_calc_capacity_remain_lasttime = current_time
# Reset-Condition
if (
self.current < utils.SOC_RESET_CURRENT
and (self.max_battery_voltage - utils.VOLTAGE_DROP <= voltageSum)
and self.soc_calc_reset_starttime
):
if (
int(current_time) - self.soc_calc_reset_starttime
) > utils.SOC_RESET_TIME:
self.soc_calc_capacity_remain = self.capacity
else:
self.soc_calc_reset_starttime = int(current_time)
else:
if self.soc_calc is None:
# if soc_calc was not stored in dbus then initialize with the soc reported by the bms
if self.soc is not None:
self.soc_calc_capacity_remain = (
self.capacity * self.soc / 100)
else:
# if there is no soc from bms then set to 100%
self.soc_calc_capacity_remain = self.capacity
else:
self.soc_calc_capacity_remain = self.capacity * self.soc_calc / 100
self.soc_calc_capacity_remain_lasttime = current_time

# Calculate the SOC based on remaining capacity
self.soc_calc = max(
min((self.soc_calc_capacity_remain / self.capacity) * 100, 100), 0
)

def prepare_voltage_management(self) -> None:
soc_reset_last_reached_days_ago = (
0
Expand Down Expand Up @@ -330,7 +393,7 @@ def manage_charge_voltage_linear(self) -> None:

# allow max voltage again, if cells are unbalanced or SoC threshold is reached
elif (
utils.SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT > self.soc
utils.SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT > self.soc_calc
or voltageDiff >= utils.CELL_VOLTAGE_DIFF_TO_RESET_VOLTAGE_LIMIT
) and not self.allow_max_voltage:
self.allow_max_voltage = True
Expand All @@ -346,10 +409,10 @@ def manage_charge_voltage_linear(self) -> None:
if utils.MAX_VOLTAGE_TIME_SEC < tDiff:
self.allow_max_voltage = False
self.max_voltage_start_time = None
if self.soc <= utils.SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT:
if self.soc_calc <= utils.SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT:
# write to log, that reset to float was not possible
logger.error(
f"Could not change to float voltage. Battery SoC ({self.soc}%) is lower"
f"Could not change to float voltage. Battery SoC ({self.soc_calc}%) is lower"
+ f" than SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT ({utils.SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT}%)."
+ " Please reset SoC manually or lower the SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT in the"
+ ' "config.ini".'
Expand Down Expand Up @@ -484,7 +547,7 @@ def manage_charge_voltage_linear(self) -> None:
)
self.charge_mode_debug += f" • penaltySum: {round(penaltySum, 3)}V"
self.charge_mode_debug += f"\ntDiff: {tDiff}/{utils.MAX_VOLTAGE_TIME_SEC}"
self.charge_mode_debug += f" • SoC: {self.soc}%"
self.charge_mode_debug += f" • SoC: {self.soc}% SoC_Calc {self.soc_calc}%"
self.charge_mode_debug += (
f" • Reset SoC: {utils.SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT}%"
)
Expand Down Expand Up @@ -551,7 +614,7 @@ def manage_charge_voltage_step(self) -> None:
# check if reset soc is greater than battery soc
# this prevents flapping between max and float voltage
elif (
utils.SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT > self.soc
utils.SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT > self.soc_calc
and not self.allow_max_voltage
):
self.allow_max_voltage = True
Expand Down Expand Up @@ -856,10 +919,10 @@ def calcMaxChargeCurrentReferringToSoc(self) -> float:
]
if utils.LINEAR_LIMITATION_ENABLE:
return utils.calcLinearRelationship(
self.soc, SOC_WHILE_CHARGING, MAX_CHARGE_CURRENT_SOC
self.soc_calc, SOC_WHILE_CHARGING, MAX_CHARGE_CURRENT_SOC
)
return utils.calcStepRelationship(
self.soc, SOC_WHILE_CHARGING, MAX_CHARGE_CURRENT_SOC, True
self.soc_calc, SOC_WHILE_CHARGING, MAX_CHARGE_CURRENT_SOC, True
)
except Exception:
return self.max_battery_charge_current
Expand All @@ -880,10 +943,10 @@ def calcMaxDischargeCurrentReferringToSoc(self) -> float:
]
if utils.LINEAR_LIMITATION_ENABLE:
return utils.calcLinearRelationship(
self.soc, SOC_WHILE_DISCHARGING, MAX_DISCHARGE_CURRENT_SOC
self.soc_calc, SOC_WHILE_DISCHARGING, MAX_DISCHARGE_CURRENT_SOC
)
return utils.calcStepRelationship(
self.soc, SOC_WHILE_DISCHARGING, MAX_DISCHARGE_CURRENT_SOC, True
self.soc_calc, SOC_WHILE_DISCHARGING, MAX_DISCHARGE_CURRENT_SOC, True
)
except Exception:
return self.max_battery_charge_current
Expand Down Expand Up @@ -941,17 +1004,17 @@ def get_cell_balancing(self, idx: int) -> Union[int, None]:
def get_capacity_remain(self) -> Union[float, None]:
if self.capacity_remain is not None:
return self.capacity_remain
if self.capacity is not None and self.soc is not None:
return self.capacity * self.soc / 100
if self.capacity is not None and self.soc_calc is not None:
return self.capacity * self.soc_calc / 100
return None

def get_timeToSoc(
self, socnum: float, crntPrctPerSec: float, onlyNumber: bool = False
) -> str:
if self.current > 0:
diffSoc = socnum - self.soc
diffSoc = socnum - self.soc_calc
else:
diffSoc = self.soc - socnum
diffSoc = self.soc_calc - socnum

"""
calculate only positive SoC points, since negative points have no sense
Expand All @@ -962,7 +1025,9 @@ def get_timeToSoc(
return None

ttgStr = None
if self.soc != socnum and (diffSoc > 0 or utils.TIME_TO_SOC_INC_FROM is True):
if self.soc_calc != socnum and (
diffSoc > 0 or utils.TIME_TO_SOC_INC_FROM is True
):
secondstogo = int(diffSoc / crntPrctPerSec)
ttgStr = ""

Expand Down Expand Up @@ -1242,7 +1307,7 @@ def log_settings(self) -> None:
logger.info(f"Battery {self.type} connected to dbus from {self.port}")
logger.info("========== Settings ==========")
logger.info(
f"> Connection voltage: {self.voltage}V | Current: {self.current}A | SoC: {self.soc}%"
f"> Connection voltage: {self.voltage}V | Current: {self.current}A | SoC: {self.soc_calc}%"
)
logger.info(
f"> Cell count: {self.cell_count} | Cells populated: {cell_counter}"
Expand Down
18 changes: 17 additions & 1 deletion etc/dbus-serialbattery/config.default.ini
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,23 @@ MAX_VOLTAGE_TIME_SEC = 900
; Linear mode: If cells are unbalanced or if SoC gets below
SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT = 80

; --------- SOC calculation ---------
; Description: Calculate the SOC in the driver. Do not use the SOC reported by the BMS
; SOC_CALCULATION = True: Calc SOC in the driver, do not use SOC reported from BMS
; - The SOC is calculated by integration of the current reported by the BMS.
; - The current reported from the BMS can be corrected by
; the map (SOC_CALC_CURRENT_MEASURED,SOC_CALC_CURRENT_REAL).
; - The SOC is set to 100% if the following conditions apply for at least SOC_RESET_TIME seconds:
; * current is lower than SOC_RESET_CURRENT Amps
; * Sum of Cell voltages >= MAX_CELL_VOLTAGE * Cell Count - VOLTAGE_DROP
; - The calculated SOC is stored in dbus to persist a driver restart
; SOC_CALCULATION = False: Use SOC reported from BMS (none of the other parameters apply)
SOC_CALCULATION = False
SOC_RESET_CURRENT = 7
SOC_RESET_TIME = 60
SOC_CALC_CURRENT_MEASURED = -300, 300
SOC_CALC_CURRENT_REAL = -300, 300


; --------- Cell Voltage Current limitation (affecting CCL/DCL) ---------
; Description: Maximal charge / discharge current will be in-/decreased depending on min and max cell voltages
Expand Down Expand Up @@ -266,7 +283,6 @@ TIME_TO_SOC_RECALCULATE_EVERY = 60
; These will be as negative time. Disabling this improves performance slightly
TIME_TO_SOC_INC_FROM = False


; --------- Additional settings ---------
; Specify one or more BMS types to load else leave empty to try to load all available
; -- Available BMS:
Expand Down
40 changes: 37 additions & 3 deletions etc/dbus-serialbattery/dbushelper.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ def __init__(self, battery):
"allow_max_voltage": self.battery.allow_max_voltage,
"max_voltage_start_time": self.battery.max_voltage_start_time,
"soc_reset_last_reached": self.battery.soc_reset_last_reached,
"soc_calc": int(self.battery.soc_calc)
if self.battery.soc_calc is not None
else "",
}

def setup_instance(self):
Expand Down Expand Up @@ -172,6 +175,12 @@ def setup_instance(self):
value["MaxVoltageStartTime"]
)

if "SocCalc" in value:
self.battery.soc_calc = float(value["SocCalc"])
logger.info(f"Soc_calc read from dbus: {int(self.battery.soc_calc)}")
else:
logger.info(f"Soc_calc not found in dbus")

if "SocResetLastReached" in value and isinstance(
value["SocResetLastReached"], int
):
Expand All @@ -194,6 +203,7 @@ def setup_instance(self):
"CustomName",
"LastSeen",
"MaxVoltageStartTime",
"SocCalc",
"SocResetLastReached",
"UniqueIdentifier",
],
Expand Down Expand Up @@ -272,6 +282,12 @@ def setup_instance(self):
0,
0,
],
"SocCalc": [
self.path_battery + "/SocCalc",
int(self.battery.soc_calc) if self.battery.soc_calc is not None else "",
0,
0,
],
"SocResetLastReached": [
self.path_battery + "/SocResetLastReached",
self.battery.soc_reset_last_reached,
Expand Down Expand Up @@ -665,9 +681,16 @@ def publish_battery(self, loop):
def publish_dbus(self):
# Update SOC, DC and System items
self._dbusservice["/System/NrOfCellsPerBattery"] = self.battery.cell_count
self._dbusservice["/Soc"] = (
round(self.battery.soc, 2) if self.battery.soc is not None else None
)
if utils.SOC_CALCULATION:
self._dbusservice["/Soc"] = (
round(self.battery.soc_calc, 2)
if self.battery.soc_calc is not None
else None
)
else:
self._dbusservice["/Soc"] = (
round(self.battery.soc, 2) if self.battery.soc is not None else None
)
self._dbusservice["/Dc/0/Voltage"] = (
round(self.battery.voltage, 2) if self.battery.voltage is not None else None
)
Expand Down Expand Up @@ -1098,6 +1121,17 @@ def saveBatteryOptions(self) -> bool:
+ f"after {self.battery.max_voltage_start_time}"
)

if int(self.battery.soc_calc) != self.save_charge_details_last["soc_calc"]:
self.save_charge_details_last["soc_calc"] = int(self.battery.soc_calc)
result = result and self.setSetting(
get_bus(),
"com.victronenergy.settings",
self.path_battery,
"SocCalc",
int(self.battery.soc_calc),
)
logger.debug(f"soc_calc written to dbus: {int(self.battery.soc_calc)}")

if (
self.battery.soc_reset_last_reached
!= self.save_charge_details_last["soc_reset_last_reached"]
Expand Down
22 changes: 22 additions & 0 deletions etc/dbus-serialbattery/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,28 @@ def _get_list_from_config(
)
TIME_TO_SOC_INC_FROM = "True" == config["DEFAULT"]["TIME_TO_SOC_INC_FROM"]

# --------- SOC calculation ---------
# Description: Calculate the SOC in the driver. Do not use the SOC reported by the BMS
# SOC_CALCULATION = True: Calc SOC in the driver, do not use SOC reported from BMS
# - The SOC is calculated by integration of the current reported by the BMS.
# - The current reported from the BMS can be corrected by
# the map (SOC_CALC_CURRENT_MEASURED,SOC_CALC_CURRENT_REAL).
# - The SOC is set to 100% if the following conditions apply for at least SOC_RESET_TIME seconds:
# * current is lower than SOC_RESET_CURRENT Amps
# * Sum of Cell voltages >= MAX_CELL_VOLTAGE * Cell Count - VOLTAGE_DROP
# - The calculated SOC is stored in dbus to persist a driver restart
# SOC_CALCULATION = False: Use SOC reported from BMS (none of the other parameters apply)

SOC_CALCULATION = "True" == config["DEFAULT"]["SOC_CALCULATION"]
SOC_RESET_CURRENT = float(config["DEFAULT"]["SOC_RESET_CURRENT"])
SOC_RESET_TIME = int(config["DEFAULT"]["SOC_RESET_TIME"])
SOC_CALC_CURRENT_MEASURED = _get_list_from_config(
"DEFAULT", "SOC_CALC_CURRENT_MEASURED", lambda v: float(v)
)
SOC_CALC_CURRENT_REAL = _get_list_from_config(
"DEFAULT", "SOC_CALC_CURRENT_REAL", lambda v: float(v)
)

# --------- Additional settings ---------
BMS_TYPE = _get_list_from_config("DEFAULT", "BMS_TYPE", lambda v: str(v))

Expand Down

0 comments on commit 1500b1b

Please sign in to comment.