diff --git a/CHANGELOG.md b/CHANGELOG.md index c6a50ae1..0b40ecd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Added: Exclude a device from beeing used by the dbus-serialbattery driver by @mr-manuel * Added: Implement callback function for update by @seidler2547 * Added: JKBMS BLE - Show last five characters from the MAC address in the custom name (which is displayed in the device list) by @mr-manuel +* Added: Load to bulk voltage every x days to reset the SoC to 100% for some BMS by @mr-manuel * Added: Save custom name and make it restart persistant by @mr-manuel * Added: Validate current, voltage, capacity and SoC for all BMS. This prevents that a device, which is no BMS, is detected as BMS. Fixes also https://github.com/Louisvdw/dbus-serialbattery/issues/479 by @mr-manuel * Changed: Enable BMS that are disabled by default by specifying it in the config file. No more need to edit scripts by @mr-manuel diff --git a/etc/dbus-serialbattery/battery.py b/etc/dbus-serialbattery/battery.py index 8080075d..349ae650 100644 --- a/etc/dbus-serialbattery/battery.py +++ b/etc/dbus-serialbattery/battery.py @@ -104,6 +104,9 @@ def init_values(self): self.cells: List[Cell] = [] self.control_charging = None self.control_voltage = None + self.bulk_requested = False + self.bulk_last_reached = 0 + self.bulk_battery_voltage = None self.max_battery_voltage = None self.min_battery_voltage = None self.allow_max_voltage = True @@ -239,8 +242,32 @@ def manage_charge_voltage(self) -> None: self.charge_mode = "Keep always max voltage" def prepare_voltage_management(self) -> None: - self.max_battery_voltage = utils.MAX_CELL_VOLTAGE * self.cell_count - self.min_battery_voltage = utils.MIN_CELL_VOLTAGE * self.cell_count + self.bulk_battery_voltage = round(utils.BULK_CELL_VOLTAGE * self.cell_count, 2) + self.max_battery_voltage = round(utils.MAX_CELL_VOLTAGE * self.cell_count, 2) + self.min_battery_voltage = round(utils.MIN_CELL_VOLTAGE * self.cell_count, 2) + + bulk_last_reached_days_ago = ( + 0 + if self.bulk_last_reached == 0 + else (((int(time()) - self.bulk_last_reached) / 60 / 60 / 24)) + ) + # set bulk_requested to True, if the days are over + # it gets set to False once the bulk voltage was reached once + if ( + utils.BULK_AFTER_DAYS is not False + and self.bulk_requested is not False + and self.allow_max_voltage + and ( + self.bulk_last_reached == 0 + or utils.BULK_AFTER_DAYS < bulk_last_reached_days_ago + ) + ): + logger.info( + f"set bulk_requested to True: first time or {utils.BULK_AFTER_DAYS}" + + f" < {round(bulk_last_reached_days_ago, 2)}" + ) + self.bulk_requested = True + self.max_battery_voltage = self.bulk_battery_voltage def manage_charge_voltage_linear(self) -> None: """ @@ -252,21 +279,32 @@ def manage_charge_voltage_linear(self) -> None: penaltySum = 0 tDiff = 0 current_time = int(time()) + # meassurment and variation tolerance in volts - measurementToleranceVariation = 0.022 - + measurementToleranceVariation = 0.025 + try: - # calculate battery sum + # calculate battery sum and check for cell overvoltage for i in range(self.cell_count): voltage = self.get_cell_voltage(i) if voltage: voltageSum += voltage # calculate penalty sum to prevent single cell overcharge by using current cell voltage - if voltage > utils.MAX_CELL_VOLTAGE: + if ( + self.max_battery_voltage != self.bulk_battery_voltage + and voltage > utils.MAX_CELL_VOLTAGE + ): # foundHighCellVoltage: reset to False is not needed, since it is recalculated every second foundHighCellVoltage = True penaltySum += voltage - utils.MAX_CELL_VOLTAGE + elif ( + self.max_battery_voltage == self.bulk_battery_voltage + and voltage > utils.BULK_CELL_VOLTAGE + ): + # foundHighCellVoltage: reset to False is not needed, since it is recalculated every second + foundHighCellVoltage = True + penaltySum += voltage - utils.BULK_CELL_VOLTAGE voltageDiff = self.get_max_cell_voltage() - self.get_min_cell_voltage() @@ -331,22 +369,30 @@ def manage_charge_voltage_linear(self) -> None: self.charge_mode = ( "Bulk dynamic" - if self.max_voltage_start_time is None + # if self.max_voltage_start_time is None # remove this line after testing + if self.max_battery_voltage == self.bulk_battery_voltage else "Absorption dynamic" ) elif self.allow_max_voltage: self.control_voltage = round(self.max_battery_voltage, 3) self.charge_mode = ( - # "Bulk" if self.max_voltage_start_time is None else "Absorption" "Bulk" - if self.max_voltage_start_time is None + # if self.max_voltage_start_time is None # remove this line after testing + if self.max_battery_voltage == self.bulk_battery_voltage else "Absorption" ) else: floatVoltage = round((utils.FLOAT_CELL_VOLTAGE * self.cell_count), 3) chargeMode = "Float" + # reset bulk when going into float + if self.bulk_requested: + logger.info("set bulk_requested to False") + self.bulk_requested = False + # IDEA: Save "bulk_last_reached" in the dbus path com.victronenergy.settings + # to make it restart persistent + self.bulk_last_reached = current_time if self.control_voltage: if not self.charge_mode.startswith("Float"): self.transition_start_time = current_time @@ -382,7 +428,7 @@ def manage_charge_voltage_linear(self) -> None: self.charge_mode += " (Linear Mode)" # uncomment for enabling debugging infos in GUI - """ + # """ self.charge_mode_debug = ( f"max_battery_voltage: {round(self.max_battery_voltage, 2)}V" ) @@ -408,7 +454,15 @@ def manage_charge_voltage_linear(self) -> None: self.charge_mode_debug += ( f"\nlinear_cvl_last_set: {self.linear_cvl_last_set}" ) - """ + self.charge_mode_debug += "\nbulk_last_reached: " + str( + "Never" + if self.bulk_last_reached == 0 + else str( + round((current_time - self.bulk_last_reached) / 60 / 60 / 24, 2) + ) + + " days ago" + ) + # """ except TypeError: self.control_voltage = None @@ -433,6 +487,7 @@ def manage_charge_voltage_step(self) -> None: """ voltageSum = 0 tDiff = 0 + current_time = int(time()) try: # calculate battery sum @@ -448,7 +503,7 @@ def manage_charge_voltage_step(self) -> None: and self.allow_max_voltage ): # example 2 - self.max_voltage_start_time = time() + self.max_voltage_start_time = current_time # check if reset soc is greater than battery soc # this prevents flapping between max and float voltage @@ -464,7 +519,7 @@ def manage_charge_voltage_step(self) -> None: # timer started else: - tDiff = time() - self.max_voltage_start_time + tDiff = current_time - self.max_voltage_start_time if utils.MAX_VOLTAGE_TIME_SEC < tDiff: self.allow_max_voltage = False self.max_voltage_start_time = None @@ -481,6 +536,11 @@ def manage_charge_voltage_step(self) -> None: else: self.control_voltage = utils.FLOAT_CELL_VOLTAGE * self.cell_count self.charge_mode = "Float" + # reset bulk when going into float + if self.bulk_requested: + logger.info("set bulk_requested to False") + self.bulk_requested = False + self.bulk_last_reached = current_time self.charge_mode += " (Step Mode)" diff --git a/etc/dbus-serialbattery/config.default.ini b/etc/dbus-serialbattery/config.default.ini index 5466b1d3..491dcb8e 100644 --- a/etc/dbus-serialbattery/config.default.ini +++ b/etc/dbus-serialbattery/config.default.ini @@ -8,10 +8,25 @@ MAX_BATTERY_DISCHARGE_CURRENT = 60.0 ; Description: Cell min/max voltages which are used to calculate the min/max battery voltage ; Example: 16 cells * 3.45V/cell = 55.2V max charge voltage. 16 cells * 2.90V = 46.4V min discharge voltage MIN_CELL_VOLTAGE = 2.900 -; Max voltage can seen as absorption voltage +; Max voltage (can seen as absorption voltage) MAX_CELL_VOLTAGE = 3.450 +; Float voltage (can be seen as resting voltage) FLOAT_CELL_VOLTAGE = 3.375 +; Bulk voltage (may be needed to reset the SoC to 100% once in a while for some BMS) +; Has to be higher as the MAX_CELL_VOLTAGE +BULK_CELL_VOLTAGE = 3.650 +; Specify after how many days the bulk voltage should be reached again +; The timer is reset when the bulk voltage is reached +; Leave empty if you don't want to use this +; Example: Value is set to 15 +; day 1: bulk reached once +; day 16: bulk reached twice +; day 31: bulk not reached since it's very cloudy +; day 34: bulk reached since the sun came out +; day 49: bulk reached again, since last time it took 3 days to reach bulk voltage +BULK_AFTER_DAYS = + ; --------- Bluetooth BMS --------- ; Description: List the Bluetooth BMS here that you want to install ; -- Available Bluetooth BMS: @@ -135,7 +150,7 @@ CCCM_SOC_ENABLE = True ; Discharge current control management enable (True/False). DCCM_SOC_ENABLE = True -; Charge current soc limits +; Charge current SoC limits CC_SOC_LIMIT1 = 98 CC_SOC_LIMIT2 = 95 CC_SOC_LIMIT3 = 91 @@ -145,7 +160,7 @@ CC_CURRENT_LIMIT1_FRACTION = 0.1 CC_CURRENT_LIMIT2_FRACTION = 0.3 CC_CURRENT_LIMIT3_FRACTION = 0.5 -; Discharge current soc limits +; Discharge current SoC limits DC_SOC_LIMIT1 = 10 DC_SOC_LIMIT2 = 20 DC_SOC_LIMIT3 = 30 diff --git a/etc/dbus-serialbattery/utils.py b/etc/dbus-serialbattery/utils.py index 46771a25..0c0c13f5 100644 --- a/etc/dbus-serialbattery/utils.py +++ b/etc/dbus-serialbattery/utils.py @@ -38,7 +38,7 @@ def _get_list_from_config( # Constants - Need to dynamically get them in future -DRIVER_VERSION = "1.0.20230625dev" +DRIVER_VERSION = "1.0.20230629dev" zero_char = chr(48) degree_sign = "\N{DEGREE SIGN}" @@ -66,6 +66,29 @@ def _get_list_from_config( ">>> ERROR: FLOAT_CELL_VOLTAGE is set to a value less than MAX_CELL_VOLTAGE. Please check the configuration." ) +# Bulk voltage (may be needed to reset the SoC to 100% once in a while for some BMS) +# Has to be higher as the MAX_CELL_VOLTAGE +BULK_CELL_VOLTAGE = float(config["DEFAULT"]["BULK_CELL_VOLTAGE"]) +if BULK_CELL_VOLTAGE < MAX_CELL_VOLTAGE: + BULK_CELL_VOLTAGE = MAX_CELL_VOLTAGE + logger.error( + ">>> ERROR: BULK_CELL_VOLTAGE is set to a value less than MAX_CELL_VOLTAGE. Please check the configuration." + ) +# Specify after how many days the bulk voltage should be reached again +# The timer is reset when the bulk voltage is reached +# Leave empty if you don't want to use this +# Example: Value is set to 15 +# day 1: bulk reached once +# day 16: bulk reached twice +# day 31: bulk not reached since it's very cloudy +# day 34: bulk reached since the sun came out +# day 49: bulk reached again, since last time it took 3 days to reach bulk voltage +BULK_AFTER_DAYS = ( + int(config["DEFAULT"]["BULK_AFTER_DAYS"]) + if config["DEFAULT"]["BULK_AFTER_DAYS"] != "" + else False +) + # --------- BMS disconnect behaviour --------- # Description: Block charge and discharge when the communication to the BMS is lost. If you are removing the # BMS on purpose, then you have to restart the driver/system to reset the block. @@ -157,6 +180,10 @@ def _get_list_from_config( CELL_VOLTAGES_WHILE_CHARGING = _get_list_from_config( "DEFAULT", "CELL_VOLTAGES_WHILE_CHARGING", lambda v: float(v) ) +if CELL_VOLTAGES_WHILE_CHARGING[0] < MAX_CELL_VOLTAGE: + logger.error( + ">>> ERROR: Maximum value of CELL_VOLTAGES_WHILE_CHARGING is set to a value lower than MAX_CELL_VOLTAGE. Please check the configuration." + ) MAX_CHARGE_CURRENT_CV = _get_list_from_config( "DEFAULT", "MAX_CHARGE_CURRENT_CV_FRACTION", @@ -166,6 +193,10 @@ def _get_list_from_config( CELL_VOLTAGES_WHILE_DISCHARGING = _get_list_from_config( "DEFAULT", "CELL_VOLTAGES_WHILE_DISCHARGING", lambda v: float(v) ) +if CELL_VOLTAGES_WHILE_DISCHARGING[0] > MIN_CELL_VOLTAGE: + logger.error( + ">>> ERROR: Minimum value of CELL_VOLTAGES_WHILE_DISCHARGING is set to a value greater than MIN_CELL_VOLTAGE. Please check the configuration." + ) MAX_DISCHARGE_CURRENT_CV = _get_list_from_config( "DEFAULT", "MAX_DISCHARGE_CURRENT_CV_FRACTION",