diff --git a/.flake8 b/.flake8 index 99572bc6..911b6b91 100644 --- a/.flake8 +++ b/.flake8 @@ -1,15 +1,15 @@ [flake8] max-line-length = 120 per-file-ignores = - ./etc/dbus-serialbattery/utils.py: E501 + ./etc/dbus-serialbattery/utils.py: E501 exclude = - ./etc/dbus-serialbattery/bms/battery_template.py, - ./etc/dbus-serialbattery/bms/mnb_test_max17853.py, - ./etc/dbus-serialbattery/bms/mnb_utils_max17853.py, - ./etc/dbus-serialbattery/bms/revov.py, - ./etc/dbus-serialbattery/minimalmodbus.py, - ./velib_python - venv + ./etc/dbus-serialbattery/bms/battery_template.py, + ./etc/dbus-serialbattery/bms/mnb_test_max17853.py, + ./etc/dbus-serialbattery/bms/mnb_utils_max17853.py, + ./etc/dbus-serialbattery/bms/revov.py, + ./etc/dbus-serialbattery/minimalmodbus.py, + ./velib_python + venv extend-ignore: - # E203 whitespace before ':' conflicts with black code formatting. Will be ignored in flake8 - E203 + # E203 whitespace before ':' conflicts with black code formatting. Will be ignored in flake8 + E203 diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml index f1634f34..d2e368c0 100644 --- a/.github/workflows/release-beta.yml +++ b/.github/workflows/release-beta.yml @@ -8,7 +8,7 @@ on: # v1.0.0alpha20230507 # v1.0.0-beta20230507 # v1.0.0-development-20230507 - - "v*.*.[0-9]+-?[a-zA-Z]*" + - "v[0-9]+.[0-9]+.[0-9]+-?[a-zA-Z]*" jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 30e52815..537f4a1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,100 @@ # Changelog +## Notes + +* The Bluetooth and CAN connections are still not stable on some systems. If you want to have a stable connection use the serial connection. + ## Breaking changes +* Driver version greater or equal to `v1.2.20240219beta` + + * The temperature limitation variables where changed to match the other variable names. + + **OLD** + + `TEMPERATURE_LIMITS_WHILE_CHARGING`, `TEMPERATURE_LIMITS_WHILE_DISCHARGING` + + **NEW** + + `TEMPERATURES_WHILE_CHARGING`, `TEMPERATURES_WHILE_DISCHARGING` + + * The SoC limitation variables where changed to match the cell voltage and temperature config. + + **OLD** + + `CC_SOC_LIMIT1`, `CC_SOC_LIMIT2`, `CC_SOC_LIMIT3` + + `CC_CURRENT_LIMIT1_FRACTION`, `CC_CURRENT_LIMIT2_FRACTION`, `CC_CURRENT_LIMIT3_FRACTION` + + `DC_SOC_LIMIT1`, `DC_SOC_LIMIT2`, `DC_SOC_LIMIT3` + + `DC_CURRENT_LIMIT1_FRACTION`, `DC_CURRENT_LIMIT2_FRACTION`, `DC_CURRENT_LIMIT3_FRACTION` + + **NEW** + + `SOC_WHILE_CHARGING`, `MAX_CHARGE_CURRENT_SOC_FRACTION`, `SOC_WHILE_DISCHARGING`, `MAX_DISCHARGE_CURRENT_SOC_FRACTION` + + +* Driver version greater or equal to `v1.1.20231223beta` + + * `PUBLISH_CONFIG_VALUES` now has to be True or False + + +* Driver version greater or equal to `v1.0.20231128beta` + + * The custom name is not saved to the config file anymore, but to the dbus service com.victronenergy.settings. You have to re-enter it once. + + * If you selected a specific device in `Settings -> System setup -> Battery monitor` and/or `Settings -> DVCC -> Controlling BMS` you have to reselect it. + + * Driver version greater or equal to `v1.0.20230629beta` and smaller or equal to `v1.0.20230926beta`: With `v1.0.20230927beta` the following values changed names: * `BULK_CELL_VOLTAGE` -> `SOC_RESET_VOLTAGE` * `BULK_AFTER_DAYS` -> `SOC_RESET_AFTER_DAYS` -## v1.0.x + +## v1.2.x + +* Added: Check if the device instance is already used by @mr-manuel +* Added: Check if there is enough space on system and data partitions before installation by @mr-manuel +* Added: LLT/JBD BLE BMS - Added MAC address as unique identifier. Fixed https://github.com/Louisvdw/dbus-serialbattery/issues/970 by @mr-manuel +* Added: Reset calculated SoC to 0%, if battery is empty by @mr-manuel +* Added: Venus OS version to logfile by @mr-manuel +* Changed: Config: SoC limitation variables where changed to match other setting variables by @mr-manuel +* Changed: Config: Temperature limitation variables where changed to match other setting variables by @mr-manuel +* Changed: Fixed showing None SoC in log in driver start by @mr-manuel +* Changed: Fixed some other errors when restoring values from dbus settings by @mr-manuel +* Changed: Fixed some SOC calculation issues by @mr-manuel +* Changed: Fixed Time-to-SoC and Time-to-Go calculation by @mr-manuel +* Changed: Install script now shows repositories and version numbers by @mr-manuel +* Changed: JKBMS BLE - Fixed driver gets unresponsive, if connection is lost https://github.com/Louisvdw/dbus-serialbattery/issues/720 with https://github.com/Louisvdw/dbus-serialbattery/pull/941 by @cupertinomiranda +* Changed: JKBMS BLE - Fixed driver not starting for some BMS models that are not sending BLE data correctly https://github.com/Louisvdw/dbus-serialbattery/issues/819 by @mr-manuel +* Changed: JKBMS BLE - Fixed temperature issue https://github.com/Louisvdw/dbus-serialbattery/issues/916 by @mr-manuel +* Changed: LLT/JBD BMS & BLE - If only one temperature is available use it as battery temp. Fixed https://github.com/Louisvdw/dbus-serialbattery/issues/971 by @mr-manuel +* Changed: Optimized reinstall-local.sh. Show installed version and restart GUI only on changes by @mr-manuel +* Changed: Reinstallation of the driver now checks, if packages are already installed for Bluetooth and CAN by @mr-manuel +* Changed: Show ForceChargingOff, ForceDischargingOff and TurnBalancingOff only for BMS that support it by @mr-manuel +* Changed: SocResetLastReached not read from dbus settings. Fixed https://github.com/Louisvdw/dbus-serialbattery/issues/840 by @mr-manuel +* Removed: Python 2 compatibility by @mr-manuel + + +## v1.1.20240121 + +* Changed: Exit the driver with error, when port is excluded in config, else the serialstarter does not continue by @mr-manuel +* Changed: Fixed issue on first driver startup, when no device setting in dbus exists by @mr-manuel +* Changed: Fixed some smaller errors by @mr-manuel +* Changed: More detailed error output when an exception happens by @mr-manuel + +### Known issues for v1.1.20240121 + +* If multiple batteries have the same `unique_identifier`, then they are displayed as one battery in the VRM portal and if you change the name, + it get changed for all dbus-serialbattries. Please change the capacity of the batteries to be unique (if the unique identifier ends with Ah) + or change the custom field on supported BMS. + E.g.: 278 Ah, 279 Ah,280 Ah,281 Ah and 282 Ah, if you have 5 batteries with 280 Ah. + + +## v1.0.20240102beta * Added: Bluetooth: Show signal strength of BMS in log by @mr-manuel * Added: Configure logging level in `config.ini` by @mr-manuel @@ -24,13 +110,20 @@ * Added: LLT/JBD BMS - Discharge / Charge Mosfet and disable / enable balancer switching over remote console/GUI with https://github.com/Louisvdw/dbus-serialbattery/pull/761 by @idstein * Added: LLT/JBD BMS - Show balancer state in GUI under the IO page with https://github.com/Louisvdw/dbus-serialbattery/pull/763 by @idstein * Added: Load to SOC reset voltage every x days to reset the SoC to 100% for some BMS by @mr-manuel +* Added: Possibility to count and calculate the SOC based on reference values with https://github.com/Louisvdw/dbus-serialbattery/pull/868 by @cflenker +* Added: Save current charge state for driver restart or device reboot. Fixes https://github.com/Louisvdw/dbus-serialbattery/issues/840 by @mr-manuel * Added: Save custom name and make it restart persistant by @mr-manuel +* Added: Setting and install logic for usb bluetooth module by @Marvo2011 * Added: Temperature names to dbus and mqtt by @mr-manuel +* Added: The device instance does not change anymore when you plug the BMS into another USB port. Fixed https://github.com/Louisvdw/dbus-serialbattery/issues/718 by @mr-manuel * Added: Use current average of the last 300 cycles for time to go and time to SoC calculation 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: `PUBLISH_CONFIG_VALUES` now has to be True or False by @mr-manuel * Changed: `VOLTAGE_DROP` now behaves differently. Before it reduced the voltage for the check, now the voltage for the charger is increased in order to get the target voltage on the BMS by @mr-manuel -* Changed: Daly BMS - Fix readsentence by @transistorgit +* Changed: Battery disconnect behaviour. See `BLOCK_ON_DISCONNECT` option in the `config.default.ini` file by @mr-manuel +* Changed: Condition for the CVL transition to float with https://github.com/Louisvdw/dbus-serialbattery/pull/895 by @cflenker * Changed: Daly BMS - Fixed https://github.com/Louisvdw/dbus-serialbattery/issues/837 by @mr-manuel +* Changed: Daly BMS - Fixed readsentence by @transistorgit * Changed: Enable BMS that are disabled by default by specifying it in the config file. No more need to edit scripts by @mr-manuel * Changed: Fixed Building wheel for dbus-fast won't finish on weak systems https://github.com/Louisvdw/dbus-serialbattery/issues/785 by @mr-manuel * Changed: Fixed error in `reinstall-local.sh` script for Bluetooth installation by @mr-manuel @@ -42,18 +135,20 @@ * Changed: Improved driver disable script by @md-manuel * Changed: Improved driver reinstall when multiple Bluetooth BMS are enabled by @mr-manuel * Changed: JKBMS - Driver do not start if manufacturer date in BMS is empty https://github.com/Louisvdw/dbus-serialbattery/issues/823 by @mr-manuel -* Changed: JKBMS_BLE BMS - Fixed MOSFET Temperature for HW 11 by @jensbehrens & @mr-manuel -* Changed: JKBMS_BLE BMS - Fixed recognition of newer models where no data is shown by @mr-manuel -* Changed: JKBMS_BLE BMS - Improved driver by @seidler2547 & @mr-manuel -* Changed: LLT/JBD BMS - Fix cycle capacity with https://github.com/Louisvdw/dbus-serialbattery/pull/762 by @idstein +* Changed: JKBMS BLE - Fixed MOSFET Temperature for HW 11 by @jensbehrens & @mr-manuel +* Changed: JKBMS BLE - Fixed recognition of newer models where no data is shown by @mr-manuel +* Changed: JKBMS BLE - Improved driver by @seidler2547 & @mr-manuel +* Changed: LLT/JBD BLE BMS recover from lost BLE connection with https://github.com/Louisvdw/dbus-serialbattery/pull/830 by @Marvo2011 +* Changed: LLT/JBD BMS - Fixed cycle capacity with https://github.com/Louisvdw/dbus-serialbattery/pull/762 by @idstein * Changed: LLT/JBD BMS - Fixed https://github.com/Louisvdw/dbus-serialbattery/issues/730 by @mr-manuel * Changed: LLT/JBD BMS - Fixed https://github.com/Louisvdw/dbus-serialbattery/issues/769 by @mr-manuel * Changed: LLT/JBD BMS - Fixed https://github.com/Louisvdw/dbus-serialbattery/issues/778 with https://github.com/Louisvdw/dbus-serialbattery/pull/798 by @idstein * Changed: LLT/JBD BMS - Improved error handling and automatical driver restart in case of error. Fixed https://github.com/Louisvdw/dbus-serialbattery/issues/777 by @mr-manuel * Changed: LLT/JBD BMS - SOC different in Xiaoxiang app and dbus-serialbattery with https://github.com/Louisvdw/dbus-serialbattery/pull/760 by @idstein * Changed: Make CCL and DCL limiting messages more clear by @mr-manuel +* Changed: Optimized CVL calculation on high cell voltage for smoother charging with https://github.com/Louisvdw/dbus-serialbattery/pull/882 by @cflenker * Changed: Reduce the big inrush current if the CVL jumps from Bulk/Absorbtion to Float https://github.com/Louisvdw/dbus-serialbattery/issues/659 by @Rikkert-RS & @ogurevich -* Changed: Sinowealth BMS - Fix not loading https://github.com/Louisvdw/dbus-serialbattery/issues/702 by @mr-manuel +* Changed: Sinowealth BMS - Fixed not loading https://github.com/Louisvdw/dbus-serialbattery/issues/702 by @mr-manuel * Changed: Time-to-Go and Time-to-SoC use the current average of the last 5 minutes for calculation by @mr-manuel * Changed: Time-to-SoC calculate only positive points by @mr-manuel * Removed: Cronjob to restart Bluetooth service every 12 hours by @mr-manuel @@ -139,16 +234,16 @@ * Changed: Disabled ANT BMS by default https://github.com/Louisvdw/dbus-serialbattery/issues/479 by @mr-manuel * Changed: Driver can now also start without serial adapter attached for Bluetooth BMS by @seidler2547 * Changed: Feedback from BMS driver to know, if BMS is found or not by @mr-manuel -* Changed: Fix for https://github.com/Louisvdw/dbus-serialbattery/issues/239 by @mr-manuel -* Changed: Fix for https://github.com/Louisvdw/dbus-serialbattery/issues/311 by @mr-manuel -* Changed: Fix for https://github.com/Louisvdw/dbus-serialbattery/issues/351 by @mr-manuel -* Changed: Fix for https://github.com/Louisvdw/dbus-serialbattery/issues/397 by @transistorgit -* Changed: Fix for https://github.com/Louisvdw/dbus-serialbattery/issues/421 by @mr-manuel -* Changed: Fix for https://github.com/Louisvdw/dbus-serialbattery/issues/450 by @mr-manuel -* Changed: Fix for https://github.com/Louisvdw/dbus-serialbattery/issues/648 by @mr-manuel * Changed: Fixed black lint errors by @mr-manuel * Changed: Fixed cell balancing background for cells 17-24 by @mr-manuel * Changed: Fixed cell balancing display for JBD/LLT BMS https://github.com/Louisvdw/dbus-serialbattery/issues/359 by @mr-manuel +* Changed: Fixed https://github.com/Louisvdw/dbus-serialbattery/issues/239 by @mr-manuel +* Changed: Fixed https://github.com/Louisvdw/dbus-serialbattery/issues/311 by @mr-manuel +* Changed: Fixed https://github.com/Louisvdw/dbus-serialbattery/issues/351 by @mr-manuel +* Changed: Fixed https://github.com/Louisvdw/dbus-serialbattery/issues/397 by @transistorgit +* Changed: Fixed https://github.com/Louisvdw/dbus-serialbattery/issues/421 by @mr-manuel +* Changed: Fixed https://github.com/Louisvdw/dbus-serialbattery/issues/450 by @mr-manuel +* Changed: Fixed https://github.com/Louisvdw/dbus-serialbattery/issues/648 by @mr-manuel * Changed: Fixed Time-To-Go is not working, if `TIME_TO_SOC_VALUE_TYPE` is set to other than `1` https://github.com/Louisvdw/dbus-serialbattery/pull/424#issuecomment-1440511018 by @mr-manuel * Changed: Improved install workflow via USB flash drive by @mr-manuel * Changed: Improved JBD BMS soc calculation https://github.com/Louisvdw/dbus-serialbattery/pull/439 by @aaronreek diff --git a/etc/dbus-serialbattery/battery.py b/etc/dbus-serialbattery/battery.py index 6a229e34..87e959fc 100644 --- a/etc/dbus-serialbattery/battery.py +++ b/etc/dbus-serialbattery/battery.py @@ -7,8 +7,6 @@ import math from time import time from abc import ABC, abstractmethod -import re -import sys class Protection(object): @@ -57,23 +55,23 @@ class Battery(ABC): use the individual implementations as type Battery and work with it. """ - def __init__(self, port, baud, address): - self.port = port - self.baud_rate = baud - self.role = "battery" - self.type = "Generic" - self.poll_interval = 1000 - self.online = True - self.hardware_version = None - self.cell_count = None + def __init__(self, port: str, baud: int, address: str): + self.port: str = port + self.baud_rate: int = baud + self.role: str = "battery" + self.type: str = "Generic" + self.poll_interval: int = 1000 + self.online: bool = True + self.hardware_version: str = None + self.cell_count: int = None # max battery charge/discharge current - self.max_battery_charge_current = None - self.max_battery_discharge_current = None - self.has_settings = 0 + self.max_battery_charge_current: float = None + self.max_battery_discharge_current: float = None + self.has_settings: bool = False # fetched from the BMS from a field where the user can input a custom string # only if available - self.custom_field = None + self.custom_field: str = None self.init_values() @@ -81,54 +79,55 @@ def init_values(self): """ Used to reset values, if battery unexpectly disconnects """ - self.voltage = None - self.current = None - self.current_avg = None - self.current_avg_lst = [] - self.capacity_remain = None - self.capacity = None - self.cycles = None - self.total_ah_drawn = None + self.voltage: float = None + self.current: float = None + self.current_avg: float = None + self.current_avg_lst: list = [] + self.capacity_remain: float = None + self.capacity: float = None + self.cycles: float = None + self.total_ah_drawn: float = None self.production = None self.protection = Protection() self.version = None - self.soc = None - self.time_to_soc_update = 0 - self.charge_fet = None - self.discharge_fet = None - self.balance_fet = None - self.temp_sensors = None - self.temp1 = None - self.temp2 = None - self.temp3 = None - self.temp4 = None - self.temp_mos = 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 + self.discharge_fet: bool = None + self.balance_fet: bool = None + self.temp_sensors: int = None + self.temp1: float = None + self.temp2: float = None + self.temp3: float = None + self.temp4: float = None + self.temp_mos: float = None self.cells: List[Cell] = [] - self.control_charging = None - self.control_voltage = None - self.soc_reset_requested = False - self.soc_reset_last_reached = 0 - self.soc_reset_battery_voltage = None - self.max_battery_voltage = None - self.min_battery_voltage = None - self.allow_max_voltage = True - self.max_voltage_start_time = None - self.transition_start_time = None - self.control_voltage_at_transition_start = None - self.charge_mode = None - self.charge_mode_debug = "" - self.charge_limitation = None - self.discharge_limitation = None - self.linear_cvl_last_set = 0 - self.linear_ccl_last_set = 0 - self.linear_dcl_last_set = 0 - self.control_current = None - self.control_previous_total = None - self.control_previous_max = None - self.control_discharge_current = None - self.control_charge_current = None - self.control_allow_charge = None - self.control_allow_discharge = None + self.control_voltage: float = None + self.soc_reset_requested: bool = False + self.soc_reset_last_reached: int = 0 # save state to preserve on restart + self.soc_reset_battery_voltage: int = None + self.max_battery_voltage: float = None + self.min_battery_voltage: float = None + self.allow_max_voltage: bool = True # save state to preserve on restart + self.max_voltage_start_time: int = None # save state to preserve on restart + self.transition_start_time: int = None + self.charge_mode: str = None + self.charge_mode_debug: str = "" + self.charge_limitation: str = None + self.discharge_limitation: str = None + self.linear_cvl_last_set: int = 0 + self.linear_ccl_last_set: int = 0 + self.linear_dcl_last_set: int = 0 + self.control_discharge_current: int = None + self.control_charge_current: int = None + self.control_allow_charge: bool = None + self.control_allow_discharge: bool = None + # list of available callbacks, in order to display the buttons in the GUI + self.available_callbacks: List[str] = [] @abstractmethod def test_connection(self) -> bool: @@ -160,16 +159,7 @@ def connection_name(self) -> str: return "Serial " + self.port def custom_name(self) -> str: - """ - Check if the custom name is present in the config file, else return default name - """ - if len(utils.CUSTOM_BATTERY_NAMES) > 0: - for name in utils.CUSTOM_BATTERY_NAMES: - tmp = name.split(":") - if tmp[0].strip() == self.port: - return tmp[1].strip() - else: - return "SerialBattery(" + self.type + ")" + return "SerialBattery(" + self.type + ")" def product_name(self) -> str: return "SerialBattery(" + self.type + ")" @@ -217,21 +207,26 @@ def to_temp(self, sensor: int, value: float) -> None: :return: """ if sensor == 0: - self.temp_mos = min(max(value, -20), 100) + self.temp_mos = round(min(max(value, -20), 100), 1) if sensor == 1: - self.temp1 = min(max(value, -20), 100) + self.temp1 = round(min(max(value, -20), 100), 1) if sensor == 2: - self.temp2 = min(max(value, -20), 100) + self.temp2 = round(min(max(value, -20), 100), 1) if sensor == 3: - self.temp3 = min(max(value, -20), 100) + self.temp3 = round(min(max(value, -20), 100), 1) if sensor == 4: - self.temp4 = min(max(value, -20), 100) + self.temp4 = round(min(max(value, -20), 100), 1) 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: @@ -243,6 +238,121 @@ 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() + voltage_sum = 0 + current_corrected = 0 + current_min_cell_voltage = self.get_min_cell_voltage() + + # calculate battery voltage from cell voltages + for i in range(self.cell_count): + voltage = self.get_cell_voltage(i) + if voltage: + voltage_sum += voltage + + if self.soc_calc_capacity_remain is not None: + # calculate real current + current_corrected = utils.calcLinearRelationship( + self.current, + utils.SOC_CALC_CURRENT_REPORTED_BY_BMS, + utils.SOC_CALC_CURRENT_MEASURED_BY_USER, + ) + + self.soc_calc_capacity_remain = ( + self.soc_calc_capacity_remain + + current_corrected + * (current_time - self.soc_calc_capacity_remain_lasttime) + / 3600 + ) + + # limit soc_calc_capacity_remain to capacity and zero + # in case 100% is reached and the battery is not fully charged + # in case 0% is reached and the battery is not fully discharged + self.soc_calc_capacity_remain = max( + min(self.soc_calc_capacity_remain, self.capacity), 0 + ) + self.soc_calc_capacity_remain_lasttime = current_time + + # execute checks only if battery is nearly fully charged + # use lowest cell voltage, since it reaches as last the max voltage + if current_min_cell_voltage > utils.MAX_CELL_VOLTAGE * 0.99: + # check if battery is fully charged + if ( + self.current < utils.SOC_RESET_CURRENT + and (self.max_battery_voltage - utils.VOLTAGE_DROP <= voltage_sum) + and self.soc_calc_reset_starttime + ): + # set soc to 100% + if ( + (int(current_time) - self.soc_calc_reset_starttime) + > utils.SOC_RESET_TIME + and self.soc_calc_capacity_remain != self.capacity + ): + logger.info("SOC set to 100%") + self.soc_calc_capacity_remain = self.capacity + else: + self.soc_calc_reset_starttime = int(current_time) + + # execute checks only if battery is nearly fully discharged + # use lowest cell voltage, since the battery should not go undervoltage + if current_min_cell_voltage < utils.MIN_CELL_VOLTAGE * 1.01: + # check if battery is fully discharged + if ( + self.current < utils.SOC_RESET_CURRENT + and ( + current_min_cell_voltage + - (utils.VOLTAGE_DROP / self.cell_count) + <= utils.MIN_CELL_VOLTAGE + ) + and self.soc_calc_reset_starttime + ): + # set soc to 0% + if ( + int(current_time) - self.soc_calc_reset_starttime + ) > utils.SOC_RESET_TIME and self.soc_calc_capacity_remain != 0: + logger.info("SOC set to 0%") + self.soc_calc_capacity_remain = 0 + else: + self.soc_calc_reset_starttime = int(current_time) + + else: + # if soc_calc is not available from dbus then initialize it + if self.soc_calc is None: + # if there is a soc from bms then use it + if self.soc is not None: + self.soc_calc_capacity_remain = self.capacity * self.soc / 100 + logger.info( + "SOC initialized from BMS and set to " + str(self.soc) + "%" + ) + # else set it to 100% + else: + self.soc_calc_capacity_remain = self.capacity + logger.info("SOC initialized and set to 100%") + else: + self.soc_calc_capacity_remain = ( + self.capacity * self.soc_calc / 100 if self.soc > 0 else 0 + ) + logger.info( + "SOC initialized from dbus and set to " + str(self.soc_calc) + "%" + ) + + self.soc_calc_capacity_remain_lasttime = current_time + + # Calculate the SOC based on remaining capacity + self.soc_calc = round( + max(min((self.soc_calc_capacity_remain / self.capacity) * 100, 100), 0), 2 + ) + + # limit SoC to 99% in absoprtion or bulk else the battery won't charge to 100% + # only needed if charging to SoC of 100% + # to test if this can be removed + # if ( + # self.charge_mode + # and ("Bulk" in self.charge_mode or "Absorption" in self.charge_mode) + # and self.soc_calc > 99 + # ): + # self.soc_calc = 99 + def prepare_voltage_management(self) -> None: soc_reset_last_reached_days_ago = ( 0 @@ -286,44 +396,45 @@ def manage_charge_voltage_linear(self) -> None: manages the charge voltage using linear interpolation by setting self.control_voltage :return: None """ - foundHighCellVoltage = False - voltageSum = 0 - penaltySum = 0 - tDiff = 0 + found_high_cell_voltage = False + voltage_sum = 0 + penalty_sum = 0 + time_diff = 0 + control_voltage = 0 current_time = int(time()) # meassurment and variation tolerance in volts - measurementToleranceVariation = 0.5 + measurement_tolerance_variation = 0.5 try: # 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 + voltage_sum += voltage # calculate penalty sum to prevent single cell overcharge by using current cell voltage if ( self.max_battery_voltage != self.soc_reset_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 + # found_high_cell_voltage: reset to False is not needed, since it is recalculated every second + found_high_cell_voltage = True + penalty_sum += voltage - utils.MAX_CELL_VOLTAGE elif ( self.max_battery_voltage == self.soc_reset_battery_voltage and voltage > utils.SOC_RESET_VOLTAGE ): - # foundHighCellVoltage: reset to False is not needed, since it is recalculated every second - foundHighCellVoltage = True - penaltySum += voltage - utils.SOC_RESET_VOLTAGE + # found_high_cell_voltage: reset to False is not needed, since it is recalculated every second + found_high_cell_voltage = True + penalty_sum += voltage - utils.SOC_RESET_VOLTAGE voltageDiff = self.get_max_cell_voltage() - self.get_min_cell_voltage() if self.max_voltage_start_time is None: # start timer, if max voltage is reached and cells are balanced if ( - self.max_battery_voltage <= voltageSum + self.max_battery_voltage <= voltage_sum and voltageDiff <= utils.CELL_VOLTAGE_DIFF_KEEP_MAX_VOLTAGE_UNTIL and self.allow_max_voltage ): @@ -331,7 +442,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 @@ -339,15 +450,18 @@ def manage_charge_voltage_linear(self) -> None: pass else: - tDiff = current_time - self.max_voltage_start_time + if voltageDiff > utils.CELL_VOLTAGE_DIFF_KEEP_MAX_VOLTAGE_TIME_RESTART: + self.max_voltage_start_time = current_time + + time_diff = current_time - self.max_voltage_start_time # keep max voltage for MAX_VOLTAGE_TIME_SEC more seconds - if utils.MAX_VOLTAGE_TIME_SEC < tDiff: + if utils.MAX_VOLTAGE_TIME_SEC < time_diff: 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".' @@ -356,26 +470,51 @@ def manage_charge_voltage_linear(self) -> None: # we don't forget to reset max_voltage_start_time wenn we going to bulk(dynamic) mode # regardless of whether we were in absorption mode or not if ( - voltageSum - < self.max_battery_voltage - measurementToleranceVariation + voltage_sum + < self.max_battery_voltage - measurement_tolerance_variation ): self.max_voltage_start_time = None + if utils.CVL_ICONTROLLER_MODE: + if self.control_voltage: + control_voltage = self.control_voltage - ( + ( + self.get_max_cell_voltage() + - ( + utils.SOC_RESET_VOLTAGE + if self.soc_reset_requested + else utils.MAX_CELL_VOLTAGE + ) + - utils.CELL_VOLTAGE_DIFF_KEEP_MAX_VOLTAGE_UNTIL + ) + * utils.CVL_ICONTROLLER_FACTOR + ) + else: + control_voltage = self.max_battery_voltage + + control_voltage = min( + max(control_voltage, self.min_battery_voltage), + self.max_battery_voltage, + ) + # INFO: battery will only switch to Absorption, if all cells are balanced. # Reach MAX_CELL_VOLTAGE * cell count if they are all balanced. - if foundHighCellVoltage and self.allow_max_voltage: + if found_high_cell_voltage and self.allow_max_voltage: # Keep penalty above min battery voltage and below max battery voltage control_voltage = round( min( max( - voltageSum - penaltySum, + voltage_sum - penalty_sum, self.min_battery_voltage, ), self.max_battery_voltage, ), 3, ) - self.set_cvl_linear(control_voltage) + if utils.CVL_ICONTROLLER_MODE: + self.control_voltage = control_voltage + else: + self.set_cvl_linear(control_voltage) self.charge_mode = ( "Bulk dynamic" @@ -387,7 +526,11 @@ def manage_charge_voltage_linear(self) -> None: self.charge_mode += " & SoC Reset" elif self.allow_max_voltage: - self.control_voltage = round(self.max_battery_voltage, 3) + if utils.CVL_ICONTROLLER_MODE: + self.control_voltage = control_voltage + else: + self.control_voltage = round(self.max_battery_voltage, 3) + self.charge_mode = ( "Bulk" if self.max_voltage_start_time is None else "Absorption" ) @@ -402,8 +545,6 @@ def manage_charge_voltage_linear(self) -> None: if self.soc_reset_requested: # logger.info("set soc_reset_requested to False") self.soc_reset_requested = False - # IDEA: Save "soc_reset_last_reached" in the dbus path com.victronenergy.settings - # to make it restart persistent self.soc_reset_last_reached = current_time if self.control_voltage: # check if battery changed from bulk/absoprtion to float @@ -442,52 +583,73 @@ 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" - ) - self.charge_mode_debug += ( - f" - VOLTAGE_DROP: {round(utils.VOLTAGE_DROP, 2)}V" - ) - self.charge_mode_debug += f"\nvoltageSum: {round(voltageSum, 2)}V" - self.charge_mode_debug += f" • voltageDiff: {round(voltageDiff, 3)}V" - self.charge_mode_debug += ( - f"\ncontrol_voltage: {round(self.control_voltage, 2)}V" - ) - 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" • Reset SoC: {utils.SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT}%" - ) - self.charge_mode_debug += f"\nallow_max_voltage: {self.allow_max_voltage}" - self.charge_mode_debug += ( - f"\nmax_voltage_start_time: {self.max_voltage_start_time}" - ) - self.charge_mode_debug += f"\ncurrent_time: {current_time}" - self.charge_mode_debug += ( - f"\nlinear_cvl_last_set: {self.linear_cvl_last_set}" - ) - soc_reset_days_ago = round( - (current_time - self.soc_reset_last_reached) / 60 / 60 / 24, 2 - ) - soc_reset_in_days = round(utils.SOC_RESET_AFTER_DAYS - soc_reset_days_ago, 2) - self.charge_mode_debug += "\nsoc_reset_last_reached: " + str( - "Never" - if self.soc_reset_last_reached == 0 - else str(soc_reset_days_ago) - + " days ago - next in " - + str(soc_reset_in_days) - + "days" - ) - # """ + if logger.isEnabledFor(logging.DEBUG): + self.charge_mode_debug = ( + f"max_battery_voltage: {round(self.max_battery_voltage, 2)}V" + ) + self.charge_mode_debug += ( + f" - VOLTAGE_DROP: {round(utils.VOLTAGE_DROP, 2)}V" + ) + self.charge_mode_debug += f"\nvoltage_sum: {round(voltage_sum, 2)}V" + self.charge_mode_debug += f" • voltageDiff: {round(voltageDiff, 3)}V" + self.charge_mode_debug += ( + f"\ncontrol_voltage: {round(self.control_voltage, 2)}V" + ) + self.charge_mode_debug += f" • penalty_sum: {round(penalty_sum, 3)}V" + self.charge_mode_debug += ( + f"\ntime_diff: {time_diff}/{utils.MAX_VOLTAGE_TIME_SEC}" + ) + self.charge_mode_debug += f" • Reset voltage limit SoC: {utils.SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT}%" + self.charge_mode_debug += ( + f"\nSoC: {self.soc}% • SoC_Calc {self.soc_calc}%" + ) + self.charge_mode_debug += ( + f"\nallow_max_voltage: {self.allow_max_voltage}" + ) + self.charge_mode_debug += ( + f"\nmax_voltage_start_time: {self.max_voltage_start_time}" + ) + self.charge_mode_debug += f"\ncurrent_time: {current_time}" + self.charge_mode_debug += ( + f"\nlinear_cvl_last_set: {self.linear_cvl_last_set}" + ) + soc_reset_days_ago = round( + (current_time - self.soc_reset_last_reached) / 60 / 60 / 24, 2 + ) + soc_reset_in_days = round( + utils.SOC_RESET_AFTER_DAYS - soc_reset_days_ago, 2 + ) + self.charge_mode_debug += "\nsoc_reset_last_reached: " + str( + "Never" + if self.soc_reset_last_reached == 0 + else str(soc_reset_days_ago) + + " d ago, next in " + + str(soc_reset_in_days) + + " d" + ) + + # soc calculation debug" + self.charge_mode_debug += ( + f"\nsoc_calc_capacity_remain: {self.soc_calc_capacity_remain}Ah" + ) + + self.charge_mode_debug += "\nsoc_calc_capacity_remain_lasttime: " + str( + int(self.soc_calc_capacity_remain_lasttime) + if self.soc_calc_capacity_remain_lasttime is not None + else "None" + ) + + self.charge_mode_debug += "\nsoc_calc_reset_starttime: " + str( + int(self.soc_calc_reset_starttime) + if self.soc_calc_reset_starttime is not None + else "None" + ) except TypeError: self.control_voltage = None self.charge_mode = "--" - def set_cvl_linear(self, control_voltage) -> bool: + def set_cvl_linear(self, control_voltage: float) -> bool: """ set CVL only once every LINEAR_RECALCULATION_EVERY seconds :return: bool @@ -504,8 +666,8 @@ def manage_charge_voltage_step(self) -> None: manages the charge voltage using a step function by setting self.control_voltage :return: None """ - voltageSum = 0 - tDiff = 0 + voltage_sum = 0 + time_diff = 0 current_time = int(time()) try: @@ -513,18 +675,18 @@ def manage_charge_voltage_step(self) -> None: for i in range(self.cell_count): voltage = self.get_cell_voltage(i) if voltage: - voltageSum += voltage + voltage_sum += voltage if self.max_voltage_start_time is None: # check if max voltage is reached and start timer to keep max voltage - if self.max_battery_voltage <= voltageSum and self.allow_max_voltage: + if self.max_battery_voltage <= voltage_sum and self.allow_max_voltage: # example 2 self.max_voltage_start_time = current_time # 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 @@ -535,8 +697,8 @@ def manage_charge_voltage_step(self) -> None: # timer started else: - tDiff = current_time - self.max_voltage_start_time - if utils.MAX_VOLTAGE_TIME_SEC < tDiff: + time_diff = current_time - self.max_voltage_start_time + if utils.MAX_VOLTAGE_TIME_SEC < time_diff: self.allow_max_voltage = False self.max_voltage_start_time = None @@ -577,7 +739,10 @@ def manage_charge_current(self) -> None: # if BMS limit is lower then config limit and therefore the values are not the same, # then the limit was also read from the BMS - if utils.MAX_BATTERY_CHARGE_CURRENT > self.max_battery_charge_current: + if ( + isinstance(self.max_battery_charge_current, (int, float)) + and utils.MAX_BATTERY_CHARGE_CURRENT > self.max_battery_charge_current + ): charge_limits.update({self.max_battery_charge_current: "BMS Settings"}) if utils.CCCM_CV_ENABLE: @@ -658,7 +823,10 @@ def manage_charge_current(self) -> None: # if BMS limit is lower then config limit and therefore the values are not the same, # then the limit was also read from the BMS - if utils.MAX_BATTERY_DISCHARGE_CURRENT > self.max_battery_discharge_current: + if ( + isinstance(self.max_battery_discharge_current, (int, float)) + and utils.MAX_BATTERY_DISCHARGE_CURRENT > self.max_battery_discharge_current + ): discharge_limits.update( {self.max_battery_discharge_current: "BMS Settings"} ) @@ -747,6 +915,9 @@ def calcMaxChargeCurrentReferringToCellVoltage(self) -> float: False, ) except Exception: + logger.warning( + "Error while executing calcMaxChargeCurrentReferringToCellVoltage(). Using default value instead." + ) return self.max_battery_charge_current def calcMaxDischargeCurrentReferringToCellVoltage(self) -> float: @@ -764,6 +935,9 @@ def calcMaxDischargeCurrentReferringToCellVoltage(self) -> float: True, ) except Exception: + logger.warning( + "Error while executing calcMaxDischargeCurrentReferringToCellVoltage(). Using default value instead." + ) return self.max_battery_charge_current def calcMaxChargeCurrentReferringToTemperature(self) -> float: @@ -776,13 +950,13 @@ def calcMaxChargeCurrentReferringToTemperature(self) -> float: if utils.LINEAR_LIMITATION_ENABLE: temps[key] = utils.calcLinearRelationship( currentMaxTemperature, - utils.TEMPERATURE_LIMITS_WHILE_CHARGING, + utils.TEMPERATURES_WHILE_CHARGING, utils.MAX_CHARGE_CURRENT_T, ) else: temps[key] = utils.calcStepRelationship( currentMaxTemperature, - utils.TEMPERATURE_LIMITS_WHILE_CHARGING, + utils.TEMPERATURES_WHILE_CHARGING, utils.MAX_CHARGE_CURRENT_T, False, ) @@ -799,13 +973,13 @@ def calcMaxDischargeCurrentReferringToTemperature(self) -> float: if utils.LINEAR_LIMITATION_ENABLE: temps[key] = utils.calcLinearRelationship( currentMaxTemperature, - utils.TEMPERATURE_LIMITS_WHILE_DISCHARGING, + utils.TEMPERATURES_WHILE_DISCHARGING, utils.MAX_DISCHARGE_CURRENT_T, ) else: temps[key] = utils.calcStepRelationship( currentMaxTemperature, - utils.TEMPERATURE_LIMITS_WHILE_DISCHARGING, + utils.TEMPERATURES_WHILE_DISCHARGING, utils.MAX_DISCHARGE_CURRENT_T, True, ) @@ -814,52 +988,43 @@ def calcMaxDischargeCurrentReferringToTemperature(self) -> float: def calcMaxChargeCurrentReferringToSoc(self) -> float: try: - # Create value list. Will more this to the settings object - SOC_WHILE_CHARGING = [ - 100, - utils.CC_SOC_LIMIT1, - utils.CC_SOC_LIMIT2, - utils.CC_SOC_LIMIT3, - ] - MAX_CHARGE_CURRENT_SOC = [ - utils.CC_CURRENT_LIMIT1, - utils.CC_CURRENT_LIMIT2, - utils.CC_CURRENT_LIMIT3, - utils.MAX_BATTERY_CHARGE_CURRENT, - ] if utils.LINEAR_LIMITATION_ENABLE: return utils.calcLinearRelationship( - self.soc, SOC_WHILE_CHARGING, MAX_CHARGE_CURRENT_SOC + self.soc_calc, + utils.SOC_WHILE_CHARGING, + utils.MAX_CHARGE_CURRENT_SOC, ) return utils.calcStepRelationship( - self.soc, SOC_WHILE_CHARGING, MAX_CHARGE_CURRENT_SOC, True + self.soc_calc, + utils.SOC_WHILE_CHARGING, + utils.MAX_CHARGE_CURRENT_SOC, + True, ) except Exception: + logger.warning( + "Error while executing calcMaxChargeCurrentReferringToSoc(). Using default value instead." + ) return self.max_battery_charge_current def calcMaxDischargeCurrentReferringToSoc(self) -> float: try: - # Create value list. Will more this to the settings object - SOC_WHILE_DISCHARGING = [ - utils.DC_SOC_LIMIT3, - utils.DC_SOC_LIMIT2, - utils.DC_SOC_LIMIT1, - ] - MAX_DISCHARGE_CURRENT_SOC = [ - utils.MAX_BATTERY_DISCHARGE_CURRENT, - utils.DC_CURRENT_LIMIT3, - utils.DC_CURRENT_LIMIT2, - utils.DC_CURRENT_LIMIT1, - ] if utils.LINEAR_LIMITATION_ENABLE: return utils.calcLinearRelationship( - self.soc, SOC_WHILE_DISCHARGING, MAX_DISCHARGE_CURRENT_SOC + self.soc_calc, + utils.SOC_WHILE_DISCHARGING, + utils.MAX_DISCHARGE_CURRENT_SOC, ) return utils.calcStepRelationship( - self.soc, SOC_WHILE_DISCHARGING, MAX_DISCHARGE_CURRENT_SOC, True + self.soc_calc, + utils.SOC_WHILE_DISCHARGING, + utils.MAX_DISCHARGE_CURRENT_SOC, + True, ) except Exception: - return self.max_battery_charge_current + logger.warning( + "Error while executing calcMaxDischargeCurrentReferringToSoc(). Using default value instead." + ) + return self.max_battery_discharge_current def get_min_cell(self) -> int: min_voltage = 9999 @@ -899,12 +1064,12 @@ def get_max_cell_desc(self) -> Union[str, None]: cell_no = self.get_max_cell() return cell_no if cell_no is None else "C" + str(cell_no + 1) - def get_cell_voltage(self, idx) -> Union[float, None]: + def get_cell_voltage(self, idx: int) -> Union[float, None]: if idx >= min(len(self.cells), self.cell_count): return None return self.cells[idx].voltage - def get_cell_balancing(self, idx) -> Union[int, None]: + def get_cell_balancing(self, idx: int) -> Union[int, None]: if idx >= min(len(self.cells), self.cell_count): return None if self.cells[idx].balance is not None and self.cells[idx].balance: @@ -914,42 +1079,48 @@ def get_cell_balancing(self, idx) -> 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, crntPrctPerSec, onlyNumber=False) -> str: + def get_timeToSoc( + self, soc_target: float, percent_per_second: float, only_number: bool = False + ) -> str: if self.current > 0: - diffSoc = socnum - self.soc + soc_diff = soc_target - self.soc_calc else: - diffSoc = self.soc - socnum + soc_diff = self.soc_calc - soc_target """ calculate only positive SoC points, since negative points have no sense when charging only points above current SoC are shown when discharging only points below current SoC are shown """ - if diffSoc < 0: + if soc_diff < 0: return None - ttgStr = None - if self.soc != socnum and (diffSoc > 0 or utils.TIME_TO_SOC_INC_FROM is True): - secondstogo = int(diffSoc / crntPrctPerSec) - ttgStr = "" + time_to_go_str = None + if ( + self.soc_calc != soc_target + and percent_per_second != 0 + and (soc_diff > 0 or utils.TIME_TO_SOC_INC_FROM is True) + ): + seconds_to_go = int(soc_diff / percent_per_second) + time_to_go_str = "" - if onlyNumber or utils.TIME_TO_SOC_VALUE_TYPE & 1: - ttgStr += str(secondstogo) - if not onlyNumber and utils.TIME_TO_SOC_VALUE_TYPE & 2: - ttgStr += " [" - if not onlyNumber and utils.TIME_TO_SOC_VALUE_TYPE & 2: - ttgStr += self.get_secondsToString(secondstogo) + if only_number or utils.TIME_TO_SOC_VALUE_TYPE & 1: + time_to_go_str += str(seconds_to_go) + if not only_number and utils.TIME_TO_SOC_VALUE_TYPE & 2: + time_to_go_str += " [" + if not only_number and utils.TIME_TO_SOC_VALUE_TYPE & 2: + time_to_go_str += self.get_secondsToString(seconds_to_go) if utils.TIME_TO_SOC_VALUE_TYPE & 1: - ttgStr += "]" + time_to_go_str += "]" - return ttgStr + return time_to_go_str - def get_secondsToString(self, timespan, precision=3) -> str: + def get_secondsToString(self, timespan: int, precision: int = 3) -> str: """ Transforms seconds to a string in the format: 1d 1h 1m 1s (Victron Style) :param precision: @@ -1060,12 +1231,6 @@ def get_balancing(self) -> int: return 1 return 0 - def get_temperatures(self) -> Union[List[float], None]: - temperatures = [self.temp1, self.temp2, self.temp3, self.temp4] - result = [(t, i) for (t, i) in enumerate(temperatures) if t is not None] - if not result: - return None - def get_temp(self) -> Union[float, None]: try: if utils.TEMP_BATTERY == 1: @@ -1213,12 +1378,15 @@ 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}" ) logger.info(f"> LINEAR LIMITATION ENABLE: {utils.LINEAR_LIMITATION_ENABLE}") + logger.info( + f"> MIN CELL VOLTAGE: {utils.MIN_CELL_VOLTAGE}V | MAX CELL VOLTAGE: {utils.MAX_CELL_VOLTAGE}V" + ) logger.info( f"> MAX BATTERY CHARGE CURRENT: {utils.MAX_BATTERY_CHARGE_CURRENT}A | " + f"MAX BATTERY DISCHARGE CURRENT: {utils.MAX_BATTERY_DISCHARGE_CURRENT}A" @@ -1237,9 +1405,6 @@ def log_settings(self) -> None: + f"MAX BATTERY DISCHARGE CURRENT: {self.max_battery_discharge_current}A (read from BMS)" ) logger.info(f"> CVCM: {utils.CVCM_ENABLE}") - logger.info( - f"> MIN CELL VOLTAGE: {utils.MIN_CELL_VOLTAGE}V | MAX CELL VOLTAGE: {utils.MAX_CELL_VOLTAGE}V" - ) logger.info( f"> CCCM CV: {str(utils.CCCM_CV_ENABLE).ljust(5)} | DCCM CV: {utils.DCCM_CV_ENABLE}" ) @@ -1253,161 +1418,21 @@ def log_settings(self) -> None: return - # save custom name to config file - def custom_name_callback(self, path, value): - try: - if path == "/CustomName": - file = open( - "/data/etc/dbus-serialbattery/" + utils.PATH_CONFIG_USER, "r" - ) - lines = file.readlines() - last = len(lines) - - # remove not allowed characters - value = value.replace(":", "").replace("=", "").replace(",", "").strip() - - # empty string to save new config file - config_file_new = "" - - # make sure we are in the [DEFAULT] section - current_line_in_default_section = False - default_section_checked = False - - # check if already exists - exists = False - - # count lines - i = 0 - # looping through the file - for line in lines: - # increment by one - i += 1 - - # stripping line break - line = line.strip() - - # check, if current line is after the [DEFAULT] section - if line == "[DEFAULT]": - current_line_in_default_section = True - - # check, if current line starts a new section - if line != "[DEFAULT]" and re.match(r"^\[.*\]", line): - # set default_section_checked to true, if it was already checked and a new section comes on - if current_line_in_default_section and not exists: - default_section_checked = True - current_line_in_default_section = False - - # check, if the current line is the last line - if i == last: - default_section_checked = True - - # insert or replace only in [DEFAULT] section - if current_line_in_default_section and re.match( - r"^CUSTOM_BATTERY_NAMES.*", line - ): - # set that the setting was found, else a new one is created - exists = True - - # remove setting name - line = re.sub( - "^CUSTOM_BATTERY_NAMES\s*=\s*", "", line # noqa: W605 - ) - - # change only the name of the current BMS - result = [] - bms_name_list = line.split(",") - for bms_name_pair in bms_name_list: - tmp = bms_name_pair.split(":") - if tmp[0] == self.port: - result.append(tmp[0] + ":" + value) - else: - result.append(bms_name_pair) - - new_line = "CUSTOM_BATTERY_NAMES = " + ",".join(result) - - else: - if default_section_checked and not exists: - exists = True - - # add before current line - if i != last: - new_line = ( - "CUSTOM_BATTERY_NAMES = " - + self.port - + ":" - + value - + "\n\n" - + line - ) - - # add at the end if last line - else: - new_line = ( - line - + "\n\n" - + "CUSTOM_BATTERY_NAMES = " - + self.port - + ":" - + value - ) - else: - new_line = line - # concatenate the new string and add an end-line break - config_file_new = config_file_new + new_line + "\n" - - # close the file - file.close() - # Open file in write mode - write_file = open( - "/data/etc/dbus-serialbattery/" + utils.PATH_CONFIG_USER, "w" - ) - # overwriting the old file contents with the new/replaced content - write_file.write(config_file_new) - # close the file - write_file.close() - - # logger.error("value (saved): " + str(value)) - - """ - # this removes all comments and tranfsorm the values to lowercase - utils.config.set( - "DEFAULT", - "CUSTOM_BATTERY_NAMES", - self.port + ":" + value, - ) - - # Writing our configuration file to 'example.ini' - with open( - "/data/etc/dbus-serialbattery/" + utils.PATH_CONFIG_USER, "w" - ) as configfile: - type(utils.config.write(configfile)) - """ - - except Exception: - exception_type, exception_object, exception_traceback = sys.exc_info() - file = exception_traceback.tb_frame.f_code.co_filename - line = exception_traceback.tb_lineno - logger.error( - f"Exception occurred: {repr(exception_object)} of type {exception_type} in {file} line #{line}" - ) - - return value - - def reset_soc_callback(self, path, value): + def reset_soc_callback(self, path: str, value: int) -> bool: # callback for handling reset soc request - return + return False # return False to indicate that the callback was not handled - def force_charging_off_callback(self, path, value): - return + def force_charging_off_callback(self, path: str, value: int) -> bool: + return False # return False to indicate that the callback was not handled - def force_discharging_off_callback(self, path, value): - return + def force_discharging_off_callback(self, path: str, value: int) -> bool: + return False # return False to indicate that the callback was not handled - def turn_balancing_off_callback(self, path, value): - return + def turn_balancing_off_callback(self, path: str, value: int) -> bool: + return False # return False to indicate that the callback was not handled - def trigger_soc_reset(self): + def trigger_soc_reset(self) -> bool: """ This method can be used to implement SOC reset when the battery is assumed to be full """ - return + return False # return False to indicate that the callback was not handled diff --git a/etc/dbus-serialbattery/bms/ant.py b/etc/dbus-serialbattery/bms/ant.py index ceb6be68..37530313 100644 --- a/etc/dbus-serialbattery/bms/ant.py +++ b/etc/dbus-serialbattery/bms/ant.py @@ -8,6 +8,7 @@ from utils import read_serial_data, logger import utils from struct import unpack_from +import sys class ANT(Battery): @@ -32,8 +33,17 @@ def test_connection(self): try: result = self.read_status_data() result = result and self.refresh_data() - except Exception as err: - logger.error(f"Unexpected {err=}, {type(err)=}") + except Exception: + ( + exception_type, + exception_object, + exception_traceback, + ) = sys.exc_info() + file = exception_traceback.tb_frame.f_code.co_filename + line = exception_traceback.tb_lineno + logger.error( + f"Exception occurred: {repr(exception_object)} of type {exception_type} in {file} line #{line}" + ) result = False return result @@ -105,9 +115,7 @@ def read_status_data(self): self.protection.voltage_cell_low = ( 2 if self.cell_min_voltage < utils.MIN_CELL_VOLTAGE - 0.1 - else 1 - if self.cell_min_voltage < utils.MIN_CELL_VOLTAGE - else 0 + else 1 if self.cell_min_voltage < utils.MIN_CELL_VOLTAGE else 0 ) self.protection.temp_high_charge = ( 1 if self.charge_fet == 3 or self.charge_fet == 6 else 0 diff --git a/etc/dbus-serialbattery/bms/battery_template.py b/etc/dbus-serialbattery/bms/battery_template.py index e859c675..98fe4204 100644 --- a/etc/dbus-serialbattery/bms/battery_template.py +++ b/etc/dbus-serialbattery/bms/battery_template.py @@ -9,6 +9,7 @@ from utils import is_bit_set, read_serial_data, logger import utils from struct import unpack_from +import sys class BatteryTemplate(Battery): @@ -29,8 +30,17 @@ def test_connection(self): result = self.read_status_data() # get first data to show in startup log, only if result is true result = result and self.refresh_data() - except Exception as err: - logger.error(f"Unexpected {err=}, {type(err)=}") + except Exception: + ( + exception_type, + exception_object, + exception_traceback, + ) = sys.exc_info() + file = exception_traceback.tb_frame.f_code.co_filename + line = exception_traceback.tb_lineno + logger.error( + f"Exception occurred: {repr(exception_object)} of type {exception_type} in {file} line #{line}" + ) result = False return result diff --git a/etc/dbus-serialbattery/bms/daly.py b/etc/dbus-serialbattery/bms/daly.py index 0c033254..38697022 100644 --- a/etc/dbus-serialbattery/bms/daly.py +++ b/etc/dbus-serialbattery/bms/daly.py @@ -6,6 +6,7 @@ from time import sleep, time from datetime import datetime from re import sub +import sys class Daly(Battery): @@ -20,7 +21,7 @@ def __init__(self, port, baud, address): self.cell_max_no = None self.poll_interval = 1000 self.type = self.BATTERYTYPE - self.has_settings = 1 + self.has_settings = True self.reset_soc = 0 self.soc_to_set = None self.runtime = 0 # TROUBLESHOOTING for no reply errors @@ -28,6 +29,11 @@ def __init__(self, port, baud, address): self.trigger_force_disable_charge = None self.cells_volts_data_lastreadbad = False self.last_charge_mode = self.charge_mode + # list of available callbacks, in order to display the buttons in the GUI + self.available_callbacks = [ + "force_charging_off_callback", + "force_discharging_off_callback", + ] # command bytes [StartFlag=A5][Address=40][Command=94][DataLength=8][8x zero bytes][checksum] command_base = b"\xA5\x40\x94\x08\x00\x00\x00\x00\x00\x00\x00\x00\x81" @@ -66,8 +72,17 @@ def test_connection(self): self.read_soc_data(ser) self.read_battery_code(ser) - except Exception as err: - logger.error(f"Unexpected {err=}, {type(err)=}") + except Exception: + ( + exception_type, + exception_object, + exception_traceback, + ) = sys.exc_info() + file = exception_traceback.tb_frame.f_code.co_filename + line = exception_traceback.tb_lineno + logger.error( + f"Exception occurred: {repr(exception_object)} of type {exception_type} in {file} line #{line}" + ) result = False # give the user a feedback that no BMS was found diff --git a/etc/dbus-serialbattery/bms/daly_can.py b/etc/dbus-serialbattery/bms/daly_can.py index 5b4927ef..a13875f0 100644 --- a/etc/dbus-serialbattery/bms/daly_can.py +++ b/etc/dbus-serialbattery/bms/daly_can.py @@ -90,7 +90,7 @@ def test_connection(self): def get_settings(self): self.capacity = BATTERY_CAPACITY - self.max_battery_current = MAX_BATTERY_CHARGE_CURRENT + self.max_battery_charge_current = MAX_BATTERY_CHARGE_CURRENT self.max_battery_discharge_current = MAX_BATTERY_DISCHARGE_CURRENT return True diff --git a/etc/dbus-serialbattery/bms/ecs.py b/etc/dbus-serialbattery/bms/ecs.py index ddfdf901..416fa1d6 100644 --- a/etc/dbus-serialbattery/bms/ecs.py +++ b/etc/dbus-serialbattery/bms/ecs.py @@ -4,6 +4,7 @@ from utils import logger import utils import minimalmodbus +import sys class Ecs(Battery): @@ -58,8 +59,17 @@ def test_connection(self): except IOError: result = False - except Exception as err: - logger.error(f"Unexpected {err=}, {type(err)=}") + except Exception: + ( + exception_type, + exception_object, + exception_traceback, + ) = sys.exc_info() + file = exception_traceback.tb_frame.f_code.co_filename + line = exception_traceback.tb_lineno + logger.error( + f"Exception occurred: {repr(exception_object)} of type {exception_type} in {file} line #{line}" + ) result = False # give the user a feedback that no BMS was found diff --git a/etc/dbus-serialbattery/bms/hlpdatabms4s.py b/etc/dbus-serialbattery/bms/hlpdatabms4s.py index 9f33dc25..b4870f05 100644 --- a/etc/dbus-serialbattery/bms/hlpdatabms4s.py +++ b/etc/dbus-serialbattery/bms/hlpdatabms4s.py @@ -4,6 +4,7 @@ import utils import serial from time import sleep +import sys class HLPdataBMS4S(Battery): @@ -20,8 +21,17 @@ def test_connection(self): result = False try: result = self.read_test_data() - except Exception as err: - logger.error(f"Unexpected {err=}, {type(err)=}") + except Exception: + ( + exception_type, + exception_object, + exception_traceback, + ) = sys.exc_info() + file = exception_traceback.tb_frame.f_code.co_filename + line = exception_traceback.tb_lineno + logger.error( + f"Exception occurred: {repr(exception_object)} of type {exception_type} in {file} line #{line}" + ) result = False # give the user a feedback that no BMS was found diff --git a/etc/dbus-serialbattery/bms/jkbms.py b/etc/dbus-serialbattery/bms/jkbms.py index 0a391d39..da279244 100644 --- a/etc/dbus-serialbattery/bms/jkbms.py +++ b/etc/dbus-serialbattery/bms/jkbms.py @@ -4,6 +4,7 @@ import utils from struct import unpack_from from re import sub +import sys class Jkbms(Battery): @@ -25,8 +26,17 @@ def test_connection(self): # Return True if success, False for failure try: return self.read_status_data() - except Exception as err: - logger.error(f"Unexpected {err=}, {type(err)=}") + except Exception: + ( + exception_type, + exception_object, + exception_traceback, + ) = sys.exc_info() + file = exception_traceback.tb_frame.f_code.co_filename + line = exception_traceback.tb_lineno + logger.error( + f"Exception occurred: {repr(exception_object)} of type {exception_type} in {file} line #{line}" + ) return False def get_settings(self): diff --git a/etc/dbus-serialbattery/bms/jkbms_ble.py b/etc/dbus-serialbattery/bms/jkbms_ble.py index 46742807..d9f049a3 100644 --- a/etc/dbus-serialbattery/bms/jkbms_ble.py +++ b/etc/dbus-serialbattery/bms/jkbms_ble.py @@ -6,6 +6,7 @@ from time import sleep, time from bms.jkbms_brn import Jkbms_Brn import os +import sys # from bleak import BleakScanner, BleakError # import asyncio @@ -16,10 +17,13 @@ class Jkbms_Ble(Battery): resetting = False def __init__(self, port, baud, address): - super(Jkbms_Ble, self).__init__(address.replace(":", "").lower(), baud, address) + # add "ble_" to the port name, since only numbers are not valid + super(Jkbms_Ble, self).__init__( + "ble_" + address.replace(":", "").lower(), baud, address + ) self.address = address self.type = self.BATTERYTYPE - self.jk = Jkbms_Brn(address) + self.jk = Jkbms_Brn(address, lambda: self.reset_bluetooth()) self.unique_identifier_tmp = "" logger.info("Init of Jkbms_Ble at " + address) @@ -69,8 +73,17 @@ def test_connection(self): if not result: logger.error("No BMS found at " + self.address) - except Exception as err: - logger.error(f"Unexpected {err=}, {type(err)=}") + except Exception: + ( + exception_type, + exception_object, + exception_traceback, + ) = sys.exc_info() + file = exception_traceback.tb_frame.f_code.co_filename + line = exception_traceback.tb_lineno + logger.error( + f"Exception occurred: {repr(exception_object)} of type {exception_type} in {file} line #{line}" + ) result = False return result @@ -171,9 +184,15 @@ def refresh_data(self): for c in range(self.cell_count): self.cells[c].voltage = st["cell_info"]["voltages"][c] - self.to_temp(0, st["cell_info"]["temperature_mos"]) - self.to_temp(1, st["cell_info"]["temperature_sensor_1"]) - self.to_temp(2, st["cell_info"]["temperature_sensor_2"]) + temp_mos = st["cell_info"]["temperature_mos"] + self.to_temp(0, temp_mos if temp_mos < 32767 else (65535 - temp_mos) * -1) + + temp1 = st["cell_info"]["temperature_sensor_1"] + self.to_temp(1, temp1 if temp1 < 32767 else (65535 - temp1) * -1) + + temp2 = st["cell_info"]["temperature_sensor_2"] + self.to_temp(1, temp2 if temp2 < 32767 else (65535 - temp2) * -1) + self.current = round(st["cell_info"]["current"], 1) self.voltage = round(st["cell_info"]["total_voltage"], 2) @@ -187,8 +206,8 @@ def refresh_data(self): self.balancing = False if st["cell_info"]["balancing_action"] == 0.000 else True self.balancing_current = ( st["cell_info"]["balancing_current"] - if st["cell_info"]["balancing_current"] < 32768 - else (65536 / 1000 - st["cell_info"]["balancing_current"]) * -1 + if st["cell_info"]["balancing_current"] < 32767 + else (65535 / 1000 - st["cell_info"]["balancing_current"]) * -1 ) self.balancing_action = st["cell_info"]["balancing_action"] diff --git a/etc/dbus-serialbattery/bms/jkbms_brn.py b/etc/dbus-serialbattery/bms/jkbms_brn.py index f0c29ee1..e81c885b 100644 --- a/etc/dbus-serialbattery/bms/jkbms_brn.py +++ b/etc/dbus-serialbattery/bms/jkbms_brn.py @@ -1,8 +1,9 @@ from struct import unpack_from, calcsize -from bleak import BleakScanner, BleakClient +from bleak import BleakScanner, BleakClient, exc from time import sleep, time import asyncio import threading +import sys # if used as standalone script then use custom logger # else import logger from utils @@ -20,6 +21,7 @@ def bytearray_to_string(data): # zero means parse all incoming data (every second) CELL_INFO_REFRESH_S = 0 CHAR_HANDLE = "0000ffe1-0000-1000-8000-00805f9b34fb" +CHAR_HANDLE_FAILOVER = 4 MODEL_NBR_UUID = "00002a24-0000-1000-8000-00805f9b34fb" COMMAND_CELL_INFO = 0x96 @@ -139,9 +141,14 @@ class Jkbms_Brn: # translate info placeholder, since it depends on the bms_max_cell_count translate_cell_info = [] - def __init__(self, addr): + def __init__(self, addr, reset_bt_callback=None): self.address = addr - self.bt_thread = threading.Thread(target=self.connect_and_scrape) + self.bt_thread = None + self.bt_thread_monitor = threading.Thread( + target=self.monitor_scraping, name="Thread-JKBMS-Monitor" + ) + self.bt_reset = reset_bt_callback + self.should_be_scraping = False self.trigger_soc_reset = False async def scanForDevices(self): @@ -401,7 +408,26 @@ async def write_register( frame[18] = 0x00 frame[19] = self.crc(frame, len(frame) - 1) logger.debug("Write register: " + str(address) + " " + str(frame)) - await bleakC.write_gatt_char(CHAR_HANDLE, frame, response=awaitresponse) + + # some JKBMS trow an error + # BleakError('Multiple Characteristics with this UUID, refer to your desired + # characteristic by the `handle` attribute instead.') + # failover in this case and use handle instead of UUID + try: + await bleakC.write_gatt_char(CHAR_HANDLE, frame, response=awaitresponse) + except exc.BleakError: + ( + exception_type, + exception_object, + exception_traceback, + ) = sys.exc_info() + logger.debug( + f'Error getting UUID "{CHAR_HANDLE}": {repr(exception_object)} -> failover' + ) + await bleakC.write_gatt_char( + CHAR_HANDLE_FAILOVER, frame, response=awaitresponse + ) + if awaitresponse: await asyncio.sleep(5) @@ -442,14 +468,44 @@ async def asy_connect_and_scrape(self): while self.run and self.main_thread.is_alive(): # autoreconnect client = BleakClient(self.address) logger.debug("--> asy_connect_and_scrape(): btloop") + try: logger.debug("--> asy_connect_and_scrape(): reconnect") await client.connect() - self.bms_status["model_nbr"] = ( - await client.read_gatt_char(MODEL_NBR_UUID) - ).decode("utf-8") - await client.start_notify(CHAR_HANDLE, self.ncallback) + # try to get MODEL_NBR_UUID, since not all JKBMS send it + try: + self.bms_status["model_nbr"] = ( + await client.read_gatt_char(MODEL_NBR_UUID) + ).decode("utf-8") + except exc.BleakError: + ( + exception_type, + exception_object, + exception_traceback, + ) = sys.exc_info() + logger.debug( + f'Error getting UUID "{MODEL_NBR_UUID}": {repr(exception_object)} -> failover' + ) + self.bms_status["model_nbr"] = "JK-BMS-Unknown-Model" + + # some JKBMS trow an error + # BleakError('Multiple Characteristics with this UUID, refer to your desired + # characteristic by the `handle` attribute instead.') + # failover in this case and use handle instead of UUID + try: + await client.start_notify(CHAR_HANDLE, self.ncallback) + except exc.BleakError: + ( + exception_type, + exception_object, + exception_traceback, + ) = sys.exc_info() + logger.debug( + f'Error getting UUID "{CHAR_HANDLE}": {repr(exception_object)} -> failover' + ) + await client.start_notify(CHAR_HANDLE_FAILOVER, self.ncallback) + await self.request_bt("device_info", client) await self.request_bt("cell_info", client) @@ -460,38 +516,77 @@ async def asy_connect_and_scrape(self): self.trigger_soc_reset = False await self.reset_soc_jk(client) await asyncio.sleep(0.01) - except Exception as err: + + except exc.BleakDeviceNotFoundError: + logger.info( + f"--> asy_connect_and_scrape(): device not found: {self.address}" + ) self.run = False + + except Exception: + ( + exception_type, + exception_object, + exception_traceback, + ) = sys.exc_info() + file = exception_traceback.tb_frame.f_code.co_filename + line = exception_traceback.tb_lineno logger.info( - f"--> asy_connect_and_scrape(): error while connecting to bt: {err}" + f"--> asy_connect_and_scrape(): error while connecting to bt: {repr(exception_object)} " + + f"of type {exception_type} in {file} line #{line}" ) + self.run = False + finally: self.run = False if client.is_connected: try: await client.disconnect() - except Exception as err: + except Exception: + ( + exception_type, + exception_object, + exception_traceback, + ) = sys.exc_info() + file = exception_traceback.tb_frame.f_code.co_filename + line = exception_traceback.tb_lineno logger.info( - f"--> asy_connect_and_scrape(): error while disconnecting: {err}" + f"--> asy_connect_and_scrape(): error while disconnecting: {repr(exception_object)} " + + f"of type {exception_type} in {file} line #{line}" ) logger.info("--> asy_connect_and_scrape(): Exit") + def monitor_scraping(self): + while self.should_be_scraping is True: + self.bt_thread = threading.Thread( + target=self.connect_and_scrape, name="Thread-JKBMS-Connect-and-Scrape" + ) + self.bt_thread.start() + logger.debug( + "scraping thread started -> main thread id: " + + str(self.main_thread.ident) + + " scraping thread: " + + str(self.bt_thread.ident) + ) + self.bt_thread.join() + if self.should_be_scraping is True: + logger.debug("scraping thread ended: reseting bluetooth and restarting") + if self.bt_reset is not None: + self.bt_reset() + sleep(2) + def start_scraping(self): self.main_thread = threading.current_thread() if self.is_running(): - logger.debug("screaping thread already running") + logger.debug("scraping thread already running") return - self.bt_thread.start() - logger.debug( - "scraping thread started -> main thread id: " - + str(self.main_thread.ident) - + " scraping thread: " - + str(self.bt_thread.ident) - ) + self.should_be_scraping = True + self.bt_thread_monitor.start() def stop_scraping(self): self.run = False + self.should_be_scraping = False stop = time() while self.is_running(): sleep(0.1) @@ -500,7 +595,9 @@ def stop_scraping(self): return True def is_running(self): - return self.bt_thread.is_alive() + if self.bt_thread is not None: + return self.bt_thread.is_alive() + return False async def enable_charging(self, c): # these are the registers for the control-buttons: @@ -552,8 +649,6 @@ async def reset_soc_jk(self, c): if __name__ == "__main__": - import sys - jk = Jkbms_Brn(sys.argv[1]) if not jk.test_connection(): logger.error(">>> ERROR: Unable to connect") diff --git a/etc/dbus-serialbattery/bms/jkbms_can.py b/etc/dbus-serialbattery/bms/jkbms_can.py index 2021771d..7a9f7da7 100644 --- a/etc/dbus-serialbattery/bms/jkbms_can.py +++ b/etc/dbus-serialbattery/bms/jkbms_can.py @@ -49,11 +49,12 @@ def __del__(self): MESSAGES_TO_READ = 100 + # Changed from 0x0XF4 to 0x0XF5. See https://github.com/Louisvdw/dbus-serialbattery/issues/950 CAN_FRAMES = { - BATT_STAT: 0x02F4, - CELL_VOLT: 0x04F4, - CELL_TEMP: 0x05F4, - ALM_INFO: 0x07F4, + BATT_STAT: 0x02F5, + CELL_VOLT: 0x04F5, + CELL_TEMP: 0x05F5, + ALM_INFO: 0x07F5, } def test_connection(self): @@ -66,7 +67,7 @@ def get_settings(self): # After successful connection get_settings will be call to set up the battery. # Set the current limits, populate cell count, etc # Return True if success, False for failure - self.max_battery_current = MAX_BATTERY_CHARGE_CURRENT + self.max_battery_charge_current = MAX_BATTERY_CHARGE_CURRENT self.max_battery_discharge_current = MAX_BATTERY_DISCHARGE_CURRENT self.max_battery_voltage = MAX_CELL_VOLTAGE * self.cell_count self.min_battery_voltage = MIN_CELL_VOLTAGE * self.cell_count diff --git a/etc/dbus-serialbattery/bms/lifepower.py b/etc/dbus-serialbattery/bms/lifepower.py index b46421fb..7b188c2c 100644 --- a/etc/dbus-serialbattery/bms/lifepower.py +++ b/etc/dbus-serialbattery/bms/lifepower.py @@ -5,6 +5,7 @@ import utils from struct import unpack_from import re +import sys class Lifepower(Battery): @@ -28,8 +29,17 @@ def test_connection(self): result = False try: result = self.read_status_data() - except Exception as err: - logger.error(f"Unexpected {err=}, {type(err)=}") + except Exception: + ( + exception_type, + exception_object, + exception_traceback, + ) = sys.exc_info() + file = exception_traceback.tb_frame.f_code.co_filename + line = exception_traceback.tb_lineno + logger.error( + f"Exception occurred: {repr(exception_object)} of type {exception_type} in {file} line #{line}" + ) result = False return result diff --git a/etc/dbus-serialbattery/bms/lltjbd.py b/etc/dbus-serialbattery/bms/lltjbd.py index 24399a40..d6a05615 100644 --- a/etc/dbus-serialbattery/bms/lltjbd.py +++ b/etc/dbus-serialbattery/bms/lltjbd.py @@ -4,6 +4,7 @@ import utils from struct import unpack_from, pack import struct +import sys # Protocol registers REG_ENTER_FACTORY = 0x00 @@ -235,7 +236,7 @@ def __init__(self, port, baud, address): self.protection = LltJbdProtection() self.type = self.BATTERYTYPE self._product_name: str = "" - self.has_settings = 0 + self.has_settings = False self.reset_soc = 100 self.soc_to_set = None self.factory_mode = False @@ -244,6 +245,12 @@ def __init__(self, port, baud, address): self.trigger_force_disable_charge = None self.trigger_disable_balancer = None self.cycle_capacity = None + # list of available callbacks, in order to display the buttons in the GUI + self.available_callbacks = [ + "force_charging_off_callback", + "force_discharging_off_callback", + "turn_balancing_off_callback", + ] # degree_sign = u'\N{DEGREE SIGN}' BATTERYTYPE = "LLT/JBD" @@ -268,8 +275,17 @@ def test_connection(self): and self.get_settings() and self.refresh_data() ) - except Exception as err: - logger.error(f"Unexpected {err=}, {type(err)=}") + except Exception: + ( + exception_type, + exception_object, + exception_traceback, + ) = sys.exc_info() + file = exception_traceback.tb_frame.f_code.co_filename + line = exception_traceback.tb_lineno + logger.error( + f"Exception occurred: {repr(exception_object)} of type {exception_type} in {file} line #{line}" + ) result = False return result @@ -288,13 +304,13 @@ def get_settings(self): self.cycle_capacity = float(unpack_from(">H", cycle_cap)[0]) charge_over_current = self.read_serial_data_llt(readCmd(REG_CHGOC)) if charge_over_current: - self.max_battery_charge_current = float( - unpack_from(">h", charge_over_current)[0] / 100.0 + self.max_battery_charge_current = abs( + float(unpack_from(">h", charge_over_current)[0] / 100.0) ) discharge_over_current = self.read_serial_data_llt(readCmd(REG_DSGOC)) if discharge_over_current: - self.max_battery_discharge_current = float( - unpack_from(">h", discharge_over_current)[0] / -100.0 + self.max_battery_discharge_current = abs( + float(unpack_from(">h", discharge_over_current)[0] / -100.0) ) func_config = self.read_serial_data_llt(readCmd(REG_FUNC_CONFIG)) if func_config: @@ -459,9 +475,7 @@ def to_protection_bits(self, byte_data): self.protection.soc_low = ( 2 if self.soc < utils.SOC_LOW_ALARM - else 1 - if self.soc < utils.SOC_LOW_WARNING - else 0 + else 1 if self.soc < utils.SOC_LOW_WARNING else 0 ) # extra protection flags for LltJbd @@ -576,8 +590,12 @@ def read_gen_data(self): t, ) return True - temp1 = unpack_from(">H", gen_data, 23 + (2 * t))[0] - self.to_temp(t, utils.kelvin_to_celsius(temp1 / 10)) + temperature = unpack_from(">H", gen_data, 23 + (2 * t))[0] + # if there is only one sensor, use it as the main temperature sensor + if self.temp_sensors == 1: + self.to_temp(1, utils.kelvin_to_celsius(temperature / 10)) + else: + self.to_temp(t, utils.kelvin_to_celsius(temperature / 10)) return True @@ -622,7 +640,7 @@ def validate_packet(data): ">>> ERROR: Invalid response packet. Expected begin packet character 0xDD" ) if status != 0x0: - logger.warn(">>> WARN: BMS rejected request. Status " + status) + logger.warn(">>> WARN: BMS rejected request. Status " + str(status)) return False if len(data) != payload_length + 7: logger.error( diff --git a/etc/dbus-serialbattery/bms/lltjbd_ble.py b/etc/dbus-serialbattery/bms/lltjbd_ble.py index 449e7d0b..16982199 100644 --- a/etc/dbus-serialbattery/bms/lltjbd_ble.py +++ b/etc/dbus-serialbattery/bms/lltjbd_ble.py @@ -5,6 +5,7 @@ import os import threading import sys +import re from asyncio import CancelledError from time import sleep from typing import Union, Optional @@ -24,8 +25,9 @@ class LltJbd_Ble(LltJbd): BATTERYTYPE = "LltJbd_Ble" def __init__(self, port: Optional[str], baud: Optional[int], address: str): + # add "ble_" to the port name, since only numbers are not valid super(LltJbd_Ble, self).__init__( - "ble" + address.replace(":", "").lower(), -1, address + "ble_" + address.replace(":", "").lower(), -1, address ) self.address = address @@ -43,6 +45,20 @@ def __init__(self, port: Optional[str], baud: Optional[int], address: str): self.response_queue: Optional[asyncio.Queue] = None self.ready_event: Optional[asyncio.Event] = None + self.hci_uart_ok = True + if not os.path.isfile("/tmp/dbus-blebattery-hciattach"): + execfile = open("/tmp/dbus-blebattery-hciattach", "w") + execpath = os.popen("ps -ww | grep hciattach | grep -v grep").read() + execpath = re.search("/usr/bin/hciattach.+", execpath) + execfile.write(execpath.group()) + execfile.close() + else: + execpath = os.popen("ps -ww | grep hciattach | grep -v grep").read() + if not execpath: + execfile = open("/tmp/dbus-blebattery-hciattach", "r") + os.system(execfile.readline()) + execfile.close() + logger.info("Init of LltJbd_Ble at " + address) def connection_name(self) -> str: @@ -64,10 +80,14 @@ async def bt_main_loop(self): exception_type, exception_object, exception_traceback = sys.exc_info() file = exception_traceback.tb_frame.f_code.co_filename line = exception_traceback.tb_lineno - logger.error( - f"BleakScanner(): Exception occurred: {repr(exception_object)} of type {exception_type} " - f"in {file} line #{line}" - ) + if "Bluetooth adapters" in repr(exception_object): + self.reset_hci_uart() + else: + logger.error( + f"BleakScanner(): Exception occurred: {repr(exception_object)} of type {exception_type} " + f"in {file} line #{line}" + ) + self.device = None await asyncio.sleep(0.5) # allow the bluetooth connection to recover @@ -131,19 +151,22 @@ def background_loop(self): asyncio.run(self.bt_main_loop()) async def async_test_connection(self): - self.ready_event = asyncio.Event() - if not self.bt_thread.is_alive(): - self.bt_thread.start() - - def shutdown_ble_atexit(thread): - self.run = False - thread.join() - - atexit.register(shutdown_ble_atexit, self.bt_thread) - try: - return await asyncio.wait_for(self.ready_event.wait(), timeout=5) - except asyncio.TimeoutError: - logger.error(">>> ERROR: Unable to connect with BLE device") + if self.hci_uart_ok: + self.ready_event = asyncio.Event() + if not self.bt_thread.is_alive(): + self.bt_thread.start() + + def shutdown_ble_atexit(thread): + self.run = False + thread.join() + + atexit.register(shutdown_ble_atexit, self.bt_thread) + try: + return await asyncio.wait_for(self.ready_event.wait(), timeout=5) + except asyncio.TimeoutError: + logger.error(">>> ERROR: Unable to connect with BLE device") + return False + else: return False def test_connection(self): @@ -172,6 +195,16 @@ def test_connection(self): return result + def unique_identifier(self) -> str: + """ + Used to identify a BMS when multiple BMS are connected + If not provided by the BMS/driver then the hardware version and capacity is used, + since it can be changed by small amounts to make a battery unique. + On +/- 5 Ah you can identify 11 batteries + """ + string = self.address.replace(":", "").lower() + return string + async def send_command(self, command) -> Union[bytearray, bool]: if not self.bt_client: logger.error(">>> ERROR: No BLE client connection - returning") @@ -201,32 +234,35 @@ def rx_callback(future: asyncio.Future, data: bytearray, sender, rx: bytearray): return result async def async_read_serial_data_llt(self, command): - try: - bt_task = asyncio.run_coroutine_threadsafe( - self.send_command(command), self.bt_loop - ) - result = await asyncio.wait_for(asyncio.wrap_future(bt_task), 20) - return result - except asyncio.TimeoutError: - logger.error(">>> ERROR: No reply - returning") - return False - except BleakDBusError: - exception_type, exception_object, exception_traceback = sys.exc_info() - file = exception_traceback.tb_frame.f_code.co_filename - line = exception_traceback.tb_lineno - logger.error( - f"BleakDBusError: {repr(exception_object)} of type {exception_type} in {file} line #{line}" - ) - self.reset_bluetooth() - return False - except Exception: - exception_type, exception_object, exception_traceback = sys.exc_info() - file = exception_traceback.tb_frame.f_code.co_filename - line = exception_traceback.tb_lineno - logger.error( - f"Exception occurred: {repr(exception_object)} of type {exception_type} in {file} line #{line}" - ) - self.reset_bluetooth() + if self.hci_uart_ok: + try: + bt_task = asyncio.run_coroutine_threadsafe( + self.send_command(command), self.bt_loop + ) + result = await asyncio.wait_for(asyncio.wrap_future(bt_task), 20) + return result + except asyncio.TimeoutError: + logger.error(">>> ERROR: No reply - returning") + return False + except BleakDBusError: + exception_type, exception_object, exception_traceback = sys.exc_info() + file = exception_traceback.tb_frame.f_code.co_filename + line = exception_traceback.tb_lineno + logger.error( + f"BleakDBusError: {repr(exception_object)} of type {exception_type} in {file} line #{line}" + ) + self.reset_bluetooth() + return False + except Exception: + exception_type, exception_object, exception_traceback = sys.exc_info() + file = exception_traceback.tb_frame.f_code.co_filename + line = exception_traceback.tb_lineno + logger.error( + f"Exception occurred: {repr(exception_object)} of type {exception_type} in {file} line #{line}" + ) + self.reset_bluetooth() + return False + else: return False def read_serial_data_llt(self, command): @@ -268,6 +304,25 @@ def reset_bluetooth(self): sleep(5) sys.exit(1) + def reset_hci_uart(self): + logger.error("Reset of hci_uart stack... Reconnecting to: " + self.address) + self.run = False + os.system("pkill -f 'hciattach'") + sleep(0.5) + os.system("rmmod hci_uart") + os.system("rmmod btbcm") + os.system("modprobe hci_uart") + os.system("modprobe btbcm") + sys.exit(1) + # execfile = open("/tmp/dbus-blebattery-hciattach", "r") + # sleep(5) + # os.system(execfile.readline()) + # os.system(execfile.readline()) + # execfile.close() + # sleep(0.5) + # os.system("bluetoothctl connect " + self.address) + # self.run = True + if __name__ == "__main__": bat = LltJbd_Ble("Foo", -1, sys.argv[1]) diff --git a/etc/dbus-serialbattery/bms/mnb.py b/etc/dbus-serialbattery/bms/mnb.py index ac95608d..55155de2 100644 --- a/etc/dbus-serialbattery/bms/mnb.py +++ b/etc/dbus-serialbattery/bms/mnb.py @@ -8,6 +8,7 @@ from battery import Protection, Battery, Cell from utils import logger from bms.mnb_utils_max17853 import data_cycle, init_max +import sys # from struct import * # from bms.mnb_test_max17853 import * # use test for testing @@ -97,8 +98,17 @@ def test_connection(self): result = False try: result = self.read_status_data() - except Exception as err: - logger.error(f"Unexpected {err=}, {type(err)=}") + except Exception: + ( + exception_type, + exception_object, + exception_traceback, + ) = sys.exc_info() + file = exception_traceback.tb_frame.f_code.co_filename + line = exception_traceback.tb_lineno + logger.error( + f"Exception occurred: {repr(exception_object)} of type {exception_type} in {file} line #{line}" + ) result = False return result diff --git a/etc/dbus-serialbattery/bms/renogy.py b/etc/dbus-serialbattery/bms/renogy.py index e920a771..a1a94628 100644 --- a/etc/dbus-serialbattery/bms/renogy.py +++ b/etc/dbus-serialbattery/bms/renogy.py @@ -4,6 +4,7 @@ import utils from struct import unpack import struct +import sys class Renogy(Battery): @@ -49,8 +50,17 @@ def test_connection(self): result = self.read_gen_data() # get first data to show in startup log result = result and self.refresh_data() - except Exception as err: - logger.error(f"Unexpected {err=}, {type(err)=}") + except Exception: + ( + exception_type, + exception_object, + exception_traceback, + ) = sys.exc_info() + file = exception_traceback.tb_frame.f_code.co_filename + line = exception_traceback.tb_lineno + logger.error( + f"Exception occurred: {repr(exception_object)} of type {exception_type} in {file} line #{line}" + ) result = False return result diff --git a/etc/dbus-serialbattery/bms/revov.py b/etc/dbus-serialbattery/bms/revov.py old mode 100755 new mode 100644 index 17764989..6db47c9d --- a/etc/dbus-serialbattery/bms/revov.py +++ b/etc/dbus-serialbattery/bms/revov.py @@ -7,6 +7,7 @@ from utils import * from struct import * import struct +import sys # Author: L Sheed # Date: 3 May 2022 @@ -57,8 +58,17 @@ def test_connection(self): # get first data to show in startup log if result: self.refresh_data() - except Exception as err: - logger.error(f"Unexpected {err=}, {type(err)=}") + except Exception: + ( + exception_type, + exception_object, + exception_traceback, + ) = sys.exc_info() + file = exception_traceback.tb_frame.f_code.co_filename + line = exception_traceback.tb_lineno + logger.error( + f"Exception occurred: {repr(exception_object)} of type {exception_type} in {file} line #{line}" + ) result = False return result diff --git a/etc/dbus-serialbattery/bms/seplos.py b/etc/dbus-serialbattery/bms/seplos.py index 0a0c3fe2..67002d91 100644 --- a/etc/dbus-serialbattery/bms/seplos.py +++ b/etc/dbus-serialbattery/bms/seplos.py @@ -3,6 +3,7 @@ from utils import logger import utils import serial +import sys class Seplos(Battery): @@ -82,8 +83,17 @@ def test_connection(self): result = False try: result = self.read_status_data() - except Exception as err: - logger.error(f"Unexpected {err=}, {type(err)=}") + except Exception: + ( + exception_type, + exception_object, + exception_traceback, + ) = sys.exc_info() + file = exception_traceback.tb_frame.f_code.co_filename + line = exception_traceback.tb_lineno + logger.error( + f"Exception occurred: {repr(exception_object)} of type {exception_type} in {file} line #{line}" + ) result = False # give the user a feedback that no BMS was found diff --git a/etc/dbus-serialbattery/bms/sinowealth.py b/etc/dbus-serialbattery/bms/sinowealth.py index 8e4e900e..735c1680 100755 --- a/etc/dbus-serialbattery/bms/sinowealth.py +++ b/etc/dbus-serialbattery/bms/sinowealth.py @@ -8,6 +8,7 @@ from utils import kelvin_to_celsius, read_serial_data, logger import utils from struct import unpack_from +import sys class Sinowealth(Battery): @@ -47,8 +48,17 @@ def test_connection(self): result = self.read_status_data() result = result and self.get_settings() result = result and self.refresh_data() - except Exception as err: - logger.error(f"Unexpected {err=}, {type(err)=}") + except Exception: + ( + exception_type, + exception_object, + exception_traceback, + ) = sys.exc_info() + file = exception_traceback.tb_frame.f_code.co_filename + line = exception_traceback.tb_lineno + logger.error( + f"Exception occurred: {repr(exception_object)} of type {exception_type} in {file} line #{line}" + ) result = False return result diff --git a/etc/dbus-serialbattery/config.default.ini b/etc/dbus-serialbattery/config.default.ini index af546e91..fb669289 100644 --- a/etc/dbus-serialbattery/config.default.ini +++ b/etc/dbus-serialbattery/config.default.ini @@ -7,71 +7,100 @@ ; DEBUG: Errors, warnings, info and debug messages are logged LOGGING = INFO + ; --------- Battery Current limits --------- MAX_BATTERY_CHARGE_CURRENT = 50.0 MAX_BATTERY_DISCHARGE_CURRENT = 60.0 + ; --------- Cell Voltages --------- -; 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 +; 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_CELL_VOLTAGE = 3.450 ; Float voltage (can be seen as resting voltage) FLOAT_CELL_VOLTAGE = 3.375 + ; --------- SOC reset voltage --------- -; Description: May be needed to reset the SoC to 100% once in a while for some BMS, because of SoC drift. -; Specify the cell voltage where the SoC should be reset to 100% by the BMS. -; - JKBMS: SoC is reset to 100% if one cell reaches OVP (over voltage protection) voltage -; As you have to adopt this value to your system, I reccomend to start with -; OVP voltage - 0.030 (see Example). -; - Try to increase (add) by 0.005 in steps, if the system does not switch to float mode, even if -; the target voltage SOC_RESET_VOLTAGE * CELL_COUNT is reached. -; - Try to decrease (lower) by 0.005 in steps, if the system hits the OVP too fast, before all -; cells could be balanced and the system goes into protection mode multiple times. -; Example: If OVP is 3.650, then start with 3.620 and increase/decrease by 0.005 -; Note: The value has to be higher as the MAX_CELL_VOLTAGE -SOC_RESET_VOLTAGE = 3.650 +; Description: +; May be needed to reset the SoC to 100% once in a while for some BMS, because of SoC drift. +; Specify the cell voltage where the SoC should be reset to 100% by the BMS. +; - JKBMS: SoC is reset to 100% if one cell reaches OVP (over voltage protection) voltage +; As you have to adopt this value to your system, I reccomend to start with +; OVP voltage - 0.030 (see Example). +; - Try to increase (add) by 0.005 in steps, if the system does not switch to float mode, even if +; the target voltage SOC_RESET_VOLTAGE * CELL_COUNT is reached. +; - Try to decrease (lower) by 0.005 in steps, if the system hits the OVP too fast, before all +; cells could be balanced and the system goes into protection mode multiple times. +; Example: +; If OVP is 3.650, then start with 3.620 and increase/decrease by 0.005 +; Note: +; The value has to be higher as the MAX_CELL_VOLTAGE +SOC_RESET_VOLTAGE = 3.650 ; Specify after how many days the soc reset voltage should be reached again ; The timer is reset when the soc reset voltage is reached ; Leave empty if you don't want to use this -; Example: Value is set to 15 -; day 1: soc reset reached once -; day 16: soc reset reached twice -; day 31: soc reset not reached since it's very cloudy -; day 34: soc reset reached since the sun came out -; day 49: soc reset reached again, since last time it took 3 days to reach soc reset voltage +; Example: +; Value is set to 15 +; day 1: soc reset reached once +; day 16: soc reset reached twice +; day 31: soc reset not reached since it's very cloudy +; day 34: soc reset reached since the sun came out +; day 49: soc reset reached again, since last time it took 3 days to reach soc reset voltage SOC_RESET_AFTER_DAYS = + ; --------- Bluetooth BMS --------- -; Description: Specify the Bluetooth BMS and it's MAC address that you want to install. Leave emty to disable -; -- Available Bluetooth BMS: -; Jkbms_Ble, LltJbd_Ble +; Description: +; Specify the Bluetooth BMS and it's MAC address that you want to install. Leave emty to disable +; Available Bluetooth BMS: +; Jkbms_Ble, LltJbd_Ble ; Example for one BMS: -; BLUETOOTH_BMS = Jkbms_Ble C8:47:8C:00:00:00 +; BLUETOOTH_BMS = Jkbms_Ble C8:47:8C:00:00:00 ; Example for multiple BMS: -; BLUETOOTH_BMS = Jkbms_Ble C8:47:8C:00:00:00, Jkbms_Ble C8:47:8C:00:00:11, Jkbms_Ble C8:47:8C:00:00:22 +; BLUETOOTH_BMS = Jkbms_Ble C8:47:8C:00:00:00, Jkbms_Ble C8:47:8C:00:00:11, Jkbms_Ble C8:47:8C:00:00:22 BLUETOOTH_BMS = + +; --------- Bluetooth use USB --------- +; Description: Some users reported issues to the built in bluetooth module, you can try to fix it with an USB +; module. After a change you have to run reinstall-local.sh and to manual reboot the device! +; The usb bluetooth module must have BLE support (bluetooth version >= 4.0) +; Other bluetooth devices such as Ruuvi tags not tested yet. +; False: Use the built in bluetooth module +; True: Disable built in bluetooth module and try to use USB module +BLUETOOTH_USE_USB = False + + ; --------- CAN BMS --------- -; Description: Specify the CAN port(s) where the BMS is connected to. Leave empty to disable -; -- Available CAN BMS: -; Daly_Can, Jkbms_Can +; Description: +; Specify the CAN port(s) where the BMS is connected to. Leave empty to disable +; Available CAN BMS: +; Daly_Can, Jkbms_Can ; Example for one CAN port: -; CAN_PORT = can0 +; CAN_PORT = can0 ; Example for multiple CAN ports: -; CAN_PORT = can0, can8, can9 +; CAN_PORT = can0, can8, can9 CAN_PORT = + ; --------- 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. -; False: Charge and discharge is not blocked on BMS communication loss -; True: Charge and discharge is blocked on BMS communication loss, it's unblocked when connection is established -; again or the driver/system is restarted +; 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. +; False: +; Charge and discharge is not blocked on BMS communication loss for 20 minutes, if cell voltages are between +; 3.25 V and 3.35 V. Else the driver block charge and discharge after 60 seconds. +; True: +; Charge and discharge is blocked on BMS communication loss, it's unblocked when connection is established +; again or the driver/system is restarted. This is the Victron Energy default behaviour. BLOCK_ON_DISCONNECT = False + ; --------- Charge mode --------- ; Choose the mode for voltage / current limitations (True / False) ; False is a step mode: This is the default with limitations on hard boundary steps @@ -83,41 +112,47 @@ LINEAR_LIMITATION_ENABLE = True ; Specify in seconds how often the linear values should be recalculated LINEAR_RECALCULATION_EVERY = 60 ; Specify in percent when the linear values should be recalculated immediately -; Example: 5 for a immediate change, when the value changes by more than 5% +; Example: +; 5 for a immediate change, when the value changes by more than 5% LINEAR_RECALCULATION_ON_PERC_CHANGE = 5 ; --------- Charge Voltage limitation (affecting CVL) --------- -; Description: Limit max charging voltage (MAX_CELL_VOLTAGE * cell count), switch from max voltage to float -; voltage (FLOAT_CELL_VOLTAGE * cell count) and back +; Description: +; Limit max charging voltage (MAX_CELL_VOLTAGE * cell count), switch from max voltage to float +; voltage (FLOAT_CELL_VOLTAGE * cell count) and back ; False: Max charging voltage is always kept ; True: Max charging voltage is reduced based on charge mode ; Step mode: After max voltage is reached for MAX_VOLTAGE_TIME_SEC it switches to float voltage. After -; SoC is below SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT it switches back to max voltage. +; SoC is below SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT it switches back to max voltage. ; Linear mode: After max voltage is reachend and cell voltage difference is smaller or equal to -; CELL_VOLTAGE_DIFF_KEEP_MAX_VOLTAGE_UNTIL it switches to float voltage after 300 (fixed) -; additional seconds. -; After cell voltage difference is greater or equal to CELL_VOLTAGE_DIFF_TO_RESET_VOLTAGE_LIMIT -; OR -; SoC is below SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT -; it switches back to max voltage. -; Example: The battery reached max voltage of 55.2V and hold it for 900 seconds, the the CVL is switched to +; CELL_VOLTAGE_DIFF_KEEP_MAX_VOLTAGE_UNTIL it switches to float voltage after MAX_VOLTAGE_TIME_SEC +; additional seconds. +; After cell voltage difference is greater or equal to CELL_VOLTAGE_DIFF_TO_RESET_VOLTAGE_LIMIT +; OR +; SoC is below SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT +; it switches back to max voltage. +; Example when set to True: +; Step mode: +; The battery reached max voltage of 55.2V and hold it for 900 seconds, the the CVL is switched to ; float voltage of 53.6V to don't stress the batteries. Allow max voltage of 55.2V again, if SoC is ; once below 80% -; OR +; Linear mode: ; The battery reached max voltage of 55.2V and the max cell difference is 0.010V, then switch to float -; voltage of 53.6V after 300 additional seconds to don't stress the batteries. Allow max voltage of +; voltage of 53.6V after 900 additional seconds to don't stress the batteries. Allow max voltage of ; 55.2V again if max cell difference is above 0.080V or SoC below 80%. ; Charge voltage control management enable (True/False). CVCM_ENABLE = True ; -- CVL reset based on cell voltage diff (linear mode) ; Specify cell voltage diff where CVL limit is kept until diff is equal or lower -CELL_VOLTAGE_DIFF_KEEP_MAX_VOLTAGE_UNTIL = 0.010 +CELL_VOLTAGE_DIFF_KEEP_MAX_VOLTAGE_UNTIL = 0.010 +; Specify cell voltage diff where MAX_VOLTAGE_TIME_SEC restarts if diff is bigger +CELL_VOLTAGE_DIFF_KEEP_MAX_VOLTAGE_TIME_RESTART = 0.013 ; Specify cell voltage diff where CVL limit is reset to max voltage, if value get above ; the cells are considered as imbalanced, if the cell diff exceeds 5% of the nominal cell voltage ; e.g. 3.2 V * 5 / 100 = 0.160 V -CELL_VOLTAGE_DIFF_TO_RESET_VOLTAGE_LIMIT = 0.080 +CELL_VOLTAGE_DIFF_TO_RESET_VOLTAGE_LIMIT = 0.080 ; -- CVL reset based on SoC option (step mode & linear mode) ; Specify how long the max voltage should be kept @@ -130,12 +165,37 @@ MAX_VOLTAGE_TIME_SEC = 900 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_REPORTED_BY_BMS, SOC_CALC_CURRENT_MEASURED_BY_USER) +; - 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 >= self.max_battery_voltage - VOLTAGE_DROP +; - The calculated SOC is stored in dbus to persist a driver restart +; False: Use SOC reported from BMS (none of the other parameters apply) +; More info: https://github.com/Louisvdw/dbus-serialbattery/pull/868 +SOC_CALCULATION = False +SOC_RESET_CURRENT = 7 +SOC_RESET_TIME = 60 +SOC_CALC_CURRENT_REPORTED_BY_BMS = -300, 300 +SOC_CALC_CURRENT_MEASURED_BY_USER = -300, 300 +; Example to set small currents to zero +; SOC_CALC_CURRENT_REPORTED_BY_BMS = -300, -0.5, 0.5, 300 +; SOC_CALC_CURRENT_MEASURED_BY_USER = -300, 0, 0, 300 + + ; --------- Cell Voltage Current limitation (affecting CCL/DCL) --------- ; Description: Maximal charge / discharge current will be in-/decreased depending on min and max cell voltages -; Example: 18 cells * 3.55V/cell = 63.9V max charge voltage -; 18 cells * 2.70V/cell = 48.6V min discharge voltage -; But in reality not all cells reach the same voltage at the same time. The (dis)charge current -; will be (in-/)decreased, if even ONE SINGLE BATTERY CELL reaches the limits +; Example: +; 18 cells * 3.55V/cell = 63.9V max charge voltage +; 18 cells * 2.70V/cell = 48.6V min discharge voltage +; But in reality not all cells reach the same voltage at the same time. The (dis)charge current +; will be (in-/)decreased, if even ONE SINGLE BATTERY CELL reaches the limits ; Charge current control management referring to cell-voltage enable (True/False). CCCM_CV_ENABLE = True @@ -144,17 +204,49 @@ DCCM_CV_ENABLE = True ; Set steps to reduce battery current ; The current will be changed linear between those steps if LINEAR_LIMITATION_ENABLE is set to True -CELL_VOLTAGES_WHILE_CHARGING = 3.55, 3.50, 3.45, 3.30 -MAX_CHARGE_CURRENT_CV_FRACTION = 0, 0.05, 0.5, 1 +CELL_VOLTAGES_WHILE_CHARGING = 3.55, 3.50, 3.45, 3.30 +MAX_CHARGE_CURRENT_CV_FRACTION = 0, 0.05, 0.5, 1 CELL_VOLTAGES_WHILE_DISCHARGING = 2.70, 2.80, 2.90, 3.10 MAX_DISCHARGE_CURRENT_CV_FRACTION = 0, 0.1, 0.5, 1 +; --------- Cell Voltage limitation (affecting CVL) --------- +; This function prevents a bad balanced battery to overcharge the cell with the highest voltage and the bms to +; switch off because of overvoltage of this cell. +; +; Example: +; 15 cells are at 3.4v, 1 cell is at 3.6v. Total voltage of battery is 54.6v and the Victron System sees no reason to +; lower the charging current as the control_voltage (Absorbtion Voltage) ist 55.2v +; In this case the Cell Voltage limitation kicks in and lowers the control_voltage to keep it close to the MAX_CELL_VOLTAGE. +; +; In theory this can also be done with CCL, but doing it with CVL has 2 advantages: +; - In a well balanced system the current can be kept quite high till the end of charge by using MAX_CELL_VOLTAGE for charging. +; - In systems with MPPTs and DC-feed-in activated the victron systems do not respect CCL, so CVL is the only way to prevent the +; highest cell in a bad balanced system from overcharging. +; +; There are 2 methods implemented to calculate CVL: +; 1. penalty_sum-Method (CVL_ICONTROLLER_MODE = False) +; The voltage-overshoot of all cells that exceed MAX_CELL_VOLTAGE is summed up and the control voltage is lowered by this "penalty_sum". +; This is calculated every LINEAR_RECALCULATION_EVERY seconds. +; In fact, this is a P-Controller. +; 2. I-Controller (CVL_ICONTROLLER_MODE = True) +; An I-Controller tries to control the voltage of the highest cell to MAX_CELL_VOLTAGE + CELL_VOLTAGE_DIFF_KEEP_MAX_VOLTAGE_UNTIL. +; (for example 3.45V+0.01V =3.46V). If the voltage of the highest cell is above this level, CVL is reduced. If the voltage is below, CVL is +; increased until cellcount*MAX_CELL_VOLTAGE. +; An I-Part of 0.2 V/Vs (CVL_ICONTROLLER_FACTOR) has proved to be a stable and fast controlling-behaviour. +; This method is not as fast as the penalty_sum-Method but usually smoother and more stable against toggeling and has no stationary deviation. +; More info: https://github.com/Louisvdw/dbus-serialbattery/pull/882 +CVL_ICONTROLLER_MODE = False +CVL_ICONTROLLER_FACTOR = 0.2 + + ; --------- Temperature limitation (affecting CCL/DCL) --------- -; Description: Maximal charge / discharge current will be in-/decreased depending on temperature -; Example: The temperature limit will be monitored to control the currents. If there are two temperature senors, -; then the worst case will be calculated and the more secure lower current will be set. +; Description: +; Maximal charge / discharge current will be in-/decreased depending on temperature +; Example: +; The temperature limit will be monitored to control the currents. If there are two temperature senors, +; then the worst case will be calculated and the more secure lower current will be set. ; Charge current control management referring to temperature enable (True/False). CCCM_T_ENABLE = True ; Charge current control management referring to temperature enable (True/False). @@ -162,55 +254,49 @@ DCCM_T_ENABLE = True ; Set steps to reduce battery current ; The current will be changed linear between those steps if LINEAR_LIMITATION_ENABLE is set to True -TEMPERATURE_LIMITS_WHILE_CHARGING = 0, 2, 5, 10, 15, 20, 35, 40, 55 -MAX_CHARGE_CURRENT_T_FRACTION = 0, 0.1, 0.2, 0.4, 0.8, 1, 1, 0.4, 0 +TEMPERATURES_WHILE_CHARGING = 0, 2, 5, 10, 15, 20, 35, 40, 55 +MAX_CHARGE_CURRENT_T_FRACTION = 0.00, 0.10, 0.20, 0.40, 0.80, 1.00, 1.00, 0.40, 0.00 -TEMPERATURE_LIMITS_WHILE_DISCHARGING = -20, 0, 5, 10, 15, 45, 55 -MAX_DISCHARGE_CURRENT_T_FRACTION = 0, 0.2, 0.3, 0.4, 1, 1, 0 +TEMPERATURES_WHILE_DISCHARGING = -20, 0, 5, 10, 15, 45, 55 +MAX_DISCHARGE_CURRENT_T_FRACTION = 0.00, 0.20, 0.30, 0.40, 1.00, 1.00, 0.00 ; --------- SOC limitation (affecting CCL/DCL) --------- -; Description: Maximal charge / discharge current will be increased / decreased depending on State of Charge, -; see CC_SOC_LIMIT1 etc. -; Example: The SoC limit will be monitored to control the currents. +; Description: +; Maximal charge / discharge current will be increased / decreased depending on State of Charge, +; see CC_SOC_LIMIT1 etc. +; Example: +; The SoC limit will be monitored to control the currents. ; Charge current control management enable (True/False). CCCM_SOC_ENABLE = True ; Discharge current control management enable (True/False). DCCM_SOC_ENABLE = True -; Charge current SoC limits -CC_SOC_LIMIT1 = 98 -CC_SOC_LIMIT2 = 95 -CC_SOC_LIMIT3 = 91 - -; Charge current limits -CC_CURRENT_LIMIT1_FRACTION = 0.1 -CC_CURRENT_LIMIT2_FRACTION = 0.3 -CC_CURRENT_LIMIT3_FRACTION = 0.5 - -; Discharge current SoC limits -DC_SOC_LIMIT1 = 10 -DC_SOC_LIMIT2 = 20 -DC_SOC_LIMIT3 = 30 +; Set steps to reduce battery current +; The current will be changed linear between those steps if LINEAR_LIMITATION_ENABLE is set to True +SOC_WHILE_CHARGING = 100, 95, 90, 85 +MAX_CHARGE_CURRENT_SOC_FRACTION = 0.00, 0.15, 0.50, 1.00 -; Discharge current limits -DC_CURRENT_LIMIT1_FRACTION = 0.1 -DC_CURRENT_LIMIT2_FRACTION = 0.3 -DC_CURRENT_LIMIT3_FRACTION = 0.5 +SOC_WHILE_DISCHARGING = 0, 5, 10, 15, 20 +MAX_DISCHARGE_CURRENT_SOC_FRACTION = 0.00, 0.10, 0.20, 0.50, 1.00 ; --------- Time-To-Go --------- -; Description: Calculates the time to go shown in the GUI -; Recalculation is done based on TIME_TO_SOC_RECALCULATE_EVERY +; Description: +; Calculates the time to go shown in the GUI +; Recalculation is done based on TIME_TO_SOC_RECALCULATE_EVERY TIME_TO_GO_ENABLE = True + ; --------- Time-To-Soc --------- -; Description: Calculates the time to a specific SoC -; Example: TIME_TO_SOC_POINTS = 50, 25, 15, 0 -; 6h 24m remaining until 50% SoC -; 17h 36m remaining until 25% SoC -; 22h 5m remaining until 15% SoC -; 28h 48m remaining until 0% SoC +; Description: +; Calculates the time to a specific SoC +; Example: +; TIME_TO_SOC_POINTS = 50, 25, 15, 0 +; 6h 24m remaining until 50% SoC +; 17h 36m remaining until 25% SoC +; 22h 5m remaining until 15% SoC +; 28h 48m remaining until 0% SoC ; Set of SoC percentages to report on dbus and MQTT. The more you specify the more it will impact system performance. ; [Valid values 0-100, comma separated list. More that 20 intervals are not recommended] ; Example: TIME_TO_SOC_POINTS = 100, 95, 90, 85, 75, 50, 25, 20, 10, 0 @@ -231,29 +317,24 @@ 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: -; Daly, Ecs, HeltecModbus, HLPdataBMS4S, Jkbms, Lifepower, LltJbd, Renogy, Seplos -; -- Available BMS, but disabled by default (just enter one or more below and it will be enabled): -; ANT, MNB, Sinowealth +; Available BMS: +; Daly, Ecs, HeltecModbus, HLPdataBMS4S, Jkbms, Lifepower, LltJbd, Renogy, Seplos +; Available BMS, but disabled by default (just enter one or more below and it will be enabled): +; ANT, MNB, Sinowealth BMS_TYPE = ; Exclute this serial devices from the driver startup -; Example: /dev/ttyUSB2, /dev/ttyUSB4 -EXCLUDED_DEVICES = - -; Enter custom battery names here or change it over the GUI ; Example: -; /dev/ttyUSB0:My first battery -; /dev/ttyUSB0:My first battery,/dev/ttyUSB1:My second battery -CUSTOM_BATTERY_NAMES = +; /dev/ttyUSB2, /dev/ttyUSB4 +EXCLUDED_DEVICES = ; Auto reset SoC ; If on, then SoC is reset to 100%, if the value switches from absorption to float voltage -; Currently only working for Daly BMS and JK BMS BLE +; Currently only working for Daly BMS and JKBMS BLE AUTO_RESET_SOC = True ; Publish the config settings to the dbus path "/Info/Config/" -PUBLISH_CONFIG_VALUES = 1 +PUBLISH_CONFIG_VALUES = True ; Select the format of cell data presented on dbus [Valid values 0,1,2,3] ; 0 Do not publish all the cells (only the min/max cell data as used by the default GX) @@ -265,7 +346,6 @@ BATTERY_CELL_DATA_FORMAT = 1 ; Simulate Midpoint graph (True/False). MIDPOINT_ENABLE = False - ; Battery temperature ; Specify how the battery temperature is assembled ; 0 Get mean of temperature sensor 1 to sensor 4 @@ -292,7 +372,8 @@ TEMP_4_NAME = Temp 4 ; -- LltJbd settings ; SoC low levels -; NOTE: SOC_LOW_WARNING is also used to calculate the Time-To-Go even if you are not using a LltJbd BMS +; Note: +; SOC_LOW_WARNING is also used to calculate the Time-To-Go even if you are not using a LltJbd BMS SOC_LOW_WARNING = 20 SOC_LOW_ALARM = 10 @@ -306,7 +387,7 @@ INVERT_CURRENT_MEASUREMENT = 1 GREENMETER_ADDRESS = 1 LIPRO_START_ADDRESS = 2 LIPRO_END_ADDRESS = 4 -LIPRO_CELL_COUNT = 15 +LIPRO_CELL_COUNT = 15 ; -- HeltecModbus (Heltec SmartBMS/YYBMS) settings ; Set the Modbus addresses from the adapters diff --git a/etc/dbus-serialbattery/dbus-serialbattery.py b/etc/dbus-serialbattery/dbus-serialbattery.py index 69c90437..daf08d82 100644 --- a/etc/dbus-serialbattery/dbus-serialbattery.py +++ b/etc/dbus-serialbattery/dbus-serialbattery.py @@ -7,10 +7,12 @@ import sys -if sys.version_info.major == 2: - import gobject -else: - from gi.repository import GLib as gobject +# not needed anymore since a few years +# removed after next release > v1.2.x +# if sys.version_info.major == 2: +# import gobject +# else: +from gi.repository import GLib as gobject # Victron packages # from ve_utils import exit_on_error @@ -146,16 +148,28 @@ def get_port() -> str: + " is excluded trough the config file" ) sleep(60) - sys.exit(0) + # exit with error in order that the serialstarter goes on + sys.exit(1) else: # just for MNB-SPI logger.info("No Port needed") return "/dev/ttyUSB9" + with open("/opt/victronenergy/version", "r") as f: + venus_version = f.readline().strip() + # show Venus OS version + logger.info("Venus OS " + venus_version) + + # show the version of the driver logger.info("dbus-serialbattery v" + str(utils.DRIVER_VERSION)) port = get_port() battery = None + + # wait some seconds to be sure that the serial connection is ready + # else the error throw a lot of timeouts + sleep(16) + if port.endswith("_Ble") and len(sys.argv) > 2: """ Import ble classes only, if it's a ble port, else the driver won't start due to missing python modules @@ -174,6 +188,7 @@ def get_port() -> str: if testbms.test_connection(): logger.info("Connection established to " + testbms.__class__.__name__) battery = testbms + elif port.startswith("can"): """ Import CAN classes only, if it's a can port, else the driver won't start due to missing python modules @@ -196,10 +211,8 @@ def get_port() -> str: ] battery = get_battery(port) + else: - # wait some seconds to be sure that the serial connection is ready - # else the error throw a lot of timeouts - sleep(16) battery = get_battery(port) # exit if no battery could be found @@ -207,6 +220,12 @@ def get_port() -> str: logger.error("ERROR >>> No battery connection at " + port) sys.exit(1) + # get SoC from battery, else None is displayed + if utils.SOC_CALCULATION: + battery.soc_calculation() + else: + battery.soc_calc = battery.soc + battery.log_settings() # Have a mainloop, so we can send/receive asynchronous calls to and from dbus diff --git a/etc/dbus-serialbattery/dbushelper.py b/etc/dbus-serialbattery/dbushelper.py index 5e47fef4..2d1609f6 100644 --- a/etc/dbus-serialbattery/dbushelper.py +++ b/etc/dbus-serialbattery/dbushelper.py @@ -2,9 +2,12 @@ import sys import os import platform -import dbus # pyright: ignore[reportMissingImports] +import dbus import traceback -from time import time +from time import sleep, time +from utils import logger, publish_config_variables +import utils +from xml.etree import ElementTree # Victron packages sys.path.insert( @@ -14,15 +17,13 @@ "/opt/victronenergy/dbus-systemcalc-py/ext/velib_python", ), ) -from vedbus import VeDbusService # noqa: E402 # pyright: ignore[reportMissingImports] -from settingsdevice import ( # noqa: E402 # pyright: ignore[reportMissingImports] +from vedbus import VeDbusService # noqa: E402 +from settingsdevice import ( # noqa: E402 SettingsDevice, ) -from utils import logger, publish_config_variables # noqa: E402 -import utils # noqa: E402 -def get_bus(): +def get_bus() -> dbus.bus.BusConnection: return ( dbus.SessionBus() if "DBUS_SESSION_BUS_ADDRESS" in os.environ @@ -31,54 +32,440 @@ def get_bus(): class DbusHelper: + EMPTY_DICT = {} + def __init__(self, battery): self.battery = battery self.instance = 1 self.settings = None self.error = {"count": 0, "timestamp_first": None, "timestamp_last": None} self.block_because_disconnect = False - self._dbusservice = VeDbusService( + self.cell_voltages_good = False + self._dbusname = ( "com.victronenergy.battery." - + self.battery.port[self.battery.port.rfind("/") + 1 :], - get_bus(), + + self.battery.port[self.battery.port.rfind("/") + 1 :] + ) + self._dbusservice = VeDbusService(self._dbusname, get_bus()) + self.bms_id = "".join( + # remove all non alphanumeric characters from the identifier + c if c.isalnum() else "_" + for c in self.battery.unique_identifier() ) + self.path_battery = None + self.save_charge_details_last = { + "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": ( + self.battery.soc_calc if self.battery.soc_calc is not None else "" + ), + } + + def create_pid_file(self) -> None: + """ + Create a pid file for the driver with the device instance as file name suffix. + Keep the file locked for the entire script runtime, to prevent another instance from running with + the same device instance. This is achieved by maintaining a reference to the "pid_file" object for + the entire script runtime storing "pid_file" as an instance variable "self.pid_file". + """ + # only used for this function + import fcntl + + # path to the PID file + pid_file_path = f"/var/tmp/dbus-serialbattery_{self.instance}.pid" + + try: + # open file in append mode to not flush content, if the file is locked + self.pid_file = open(pid_file_path, "a") + + # try to lock the file + fcntl.flock(self.pid_file, fcntl.LOCK_EX | fcntl.LOCK_NB) + + # fail, if the file is already locked + except OSError: + logger.error( + "** DRIVER STOPPED! Another battery with the same serial number/unique identifier " + + f'"{self.battery.unique_identifier()}" found! **' + ) + logger.error("Please check that the batteries have unique identifiers.") + + if "Ah" in self.battery.unique_identifier(): + logger.error("Change the battery capacities to be unique.") + logger.error("Example for batteries with 280 Ah:") + logger.error("- Battery 1: 279 Ah") + logger.error("- Battery 2: 280 Ah") + logger.error("- Battery 3: 281 Ah") + logger.error("This little difference does not matter for the battery.") + else: + logger.error( + "Change the customizable field in your BMS settings to be unique." + ) + logger.error( + "To see which battery already uses this serial number/unique identifier check " + + f'this file "{pid_file_path}"' + ) + + self.pid_file.close() + sleep(60) + sys.exit(1) + + except Exception: + ( + exception_type, + exception_object, + exception_traceback, + ) = sys.exc_info() + file = exception_traceback.tb_frame.f_code.co_filename + line = exception_traceback.tb_lineno + logger.error( + f"Exception occurred: {repr(exception_object)} of type {exception_type} in {file} line #{line}" + ) + + # Seek to the beginning of the file + self.pid_file.seek(0) + # Truncate the file to 0 bytes + self.pid_file.truncate() + # Write content to file + self.pid_file.write(f"{self._dbusname}:{os.getpid()}\n") + # Flush the file buffer + self.pid_file.flush() + + # Ensure the changes are written to the disk + # os.fsync(self.pid_file.fileno()) + + logger.info(f"PID file created successfully: {pid_file_path}") def setup_instance(self): + """ + Sets up the instance of the battery by checking if it was already connected once. + If the battery was already connected, it retrieves the instance from the dbus settings and + updates the last seen time. + If the battery was not connected before, it creates the settings and sets the instance to the + next available one. + """ + # bms_id = self.battery.production if self.battery.production is not None else \ # self.battery.port[self.battery.port.rfind('/') + 1:] - bms_id = self.battery.port[self.battery.port.rfind("/") + 1 :] - path = "/Settings/Devices/serialbattery" - default_instance = "battery:1" + # bms_id = self.battery.port[self.battery.port.rfind("/") + 1 :] + logger.debug("setup_instance(): start") + + custom_name = self.battery.custom_name() + device_instance = "1" + device_instances_used = [] + found_bms = False + self.path_battery = "/Settings/Devices/serialbattery" + "_" + str(self.bms_id) + + # prepare settings class + self.settings = SettingsDevice( + get_bus(), self.EMPTY_DICT, self.handle_changed_setting + ) + logger.debug("setup_instance(): SettingsDevice") + + # get all the settings from the dbus + settings_from_dbus = self.getSettingsWithValues( + get_bus(), + "com.victronenergy.settings", + "/Settings/Devices", + ) + logger.debug("setup_instance(): getSettingsWithValues") + # output: + # { + # "Settings": { + # "Devices": { + # "serialbattery_JK_B2A20S20P": { + # "AllowMaxVoltage", + # "ClassAndVrmInstance": "battery:3", + # "CustomName": "My Battery 1", + # "LastSeen": "1700926114", + # "MaxVoltageStartTime": "", + # "SocResetLastReached": 0, + # "UniqueIdentifier": "JK_B2A20S20P", + # }, + # "serialbattery_JK_B2A20S25P": { + # "AllowMaxVoltage", + # "ClassAndVrmInstance": "battery:4", + # "CustomName": "My Battery 2", + # "LastSeen": "1700926114", + # "MaxVoltageStartTime": "", + # "SocResetLastReached": 0, + # "UniqueIdentifier": "JK_B2A20S25P", + # }, + # "serialbattery_ttyUSB0": { + # "ClassAndVrmInstance": "battery:1", + # }, + # "serialbattery_ttyUSB1": { + # "ClassAndVrmInstance": "battery:2", + # }, + # "vegps_ttyUSB0": { + # "ClassAndVrmInstance": "gps:0" + # }, + # } + # } + # } + + # loop through devices in dbus settings + if ( + "Settings" in settings_from_dbus + and "Devices" in settings_from_dbus["Settings"] + ): + for key, value in settings_from_dbus["Settings"]["Devices"].items(): + # check if it's a serialbattery + if "serialbattery" in key: + # check used device instances + if "ClassAndVrmInstance" in value: + device_instances_used.append( + value["ClassAndVrmInstance"][ + value["ClassAndVrmInstance"].rfind(":") + 1 : + ] + ) + + # check the unique identifier, if the battery was already connected once + # if so, get the last saved data + if ( + "UniqueIdentifier" in value + and value["UniqueIdentifier"] == self.bms_id + ): + # set found_bms to true + found_bms = True + + # get the instance from the object name + device_instance = int( + value["ClassAndVrmInstance"][ + value["ClassAndVrmInstance"].rfind(":") + 1 : + ] + ) + logger.info( + f"Found existing battery with DeviceInstance = {device_instance}" + ) + + # check if the battery has AllowMaxVoltage set + if ( + "AllowMaxVoltage" in value + and value["AllowMaxVoltage"] != "" + ): + + try: + self.battery.allow_max_voltage = ( + True + if int(value["AllowMaxVoltage"]) == 1 + else False + ) + except Exception: + logger.error( + "AllowMaxVoltage could not be converted to type int: " + + str(value["AllowMaxVoltage"]) + ) + pass + + # check if the battery has CustomName set + if "CustomName" in value and value["CustomName"] != "": + custom_name = value["CustomName"] + + # check if the battery has MaxVoltageStartTime set + if ( + "MaxVoltageStartTime" in value + and value["MaxVoltageStartTime"] != "" + ): + try: + self.battery.max_voltage_start_time = int( + value["MaxVoltageStartTime"] + ) + except Exception: + logger.error( + "MaxVoltageStartTime could not be converted to type int: " + + str(value["MaxVoltageStartTime"]) + ) + pass + + # check if the battery has SocCalc set + # load SOC from dbus only if SOC_CALCULATION is enabled + if utils.SOC_CALCULATION: + if "SocCalc" in value: + try: + self.battery.soc_calc = float(value["SocCalc"]) + logger.debug( + f"Soc_calc read from dbus: {self.battery.soc_calc}" + ) + except Exception: + logger.error( + "SocCalc could not be converted to type float: " + + str(value["SocCalc"]) + ) + pass + else: + logger.debug("Soc_calc not found in dbus") + + # check if the battery has SocResetLastReached set + if ( + "SocResetLastReached" in value + and value["SocResetLastReached"] != "" + ): + try: + self.battery.soc_reset_last_reached = int( + value["SocResetLastReached"] + ) + except Exception: + logger.error( + "SocResetLastReached could not be converted to type int: " + + str(value["SocResetLastReached"]) + ) + pass + + # check the last seen time and remove the battery it it was not seen for 30 days + elif "LastSeen" in value and int(value["LastSeen"]) < int( + time() + ) - (60 * 60 * 24 * 30): + # remove entry + del_return = self.removeSetting( + get_bus(), + "com.victronenergy.settings", + "/Settings/Devices/" + key, + [ + "AllowMaxVoltage", + "ClassAndVrmInstance", + "CustomName", + "LastSeen", + "MaxVoltageStartTime", + "SocCalc", + "SocResetLastReached", + "UniqueIdentifier", + ], + ) + logger.info( + f"Remove /Settings/Devices/{key} from dbus. Delete result: {del_return}" + ) + + # check if the battery has a last seen time, if not then it's an old entry and can be removed + elif "LastSeen" not in value: + del_return = self.removeSetting( + get_bus(), + "com.victronenergy.settings", + "/Settings/Devices/" + key, + ["ClassAndVrmInstance"], + ) + logger.info( + f"Remove /Settings/Devices/{key} from dbus. " + + f"Old entry. Delete result: {del_return}" + ) + + if "ruuvi" in key: + # check if Ruuvi tag is enabled, if not remove entry. + if ( + "Enabled" in value + and value["Enabled"] == "0" + and "ClassAndVrmInstance" not in value + ): + del_return = self.removeSetting( + get_bus(), + "com.victronenergy.settings", + "/Settings/Devices/" + key, + ["CustomName", "Enabled", "TemperatureType"], + ) + logger.info( + f"Remove /Settings/Devices/{key} from dbus. " + + f"Ruuvi tag was disabled and had no ClassAndVrmInstance. Delete result: {del_return}" + ) + + logger.debug("setup_instance(): for loop ended") + + # create class and crm instance + class_and_vrm_instance = "battery:" + str(device_instance) + + # preare settings and write them to com.victronenergy.settings settings = { - "instance": [ - path + "_" + str(bms_id).replace(" ", "_") + "/ClassAndVrmInstance", - default_instance, + "AllowMaxVoltage": [ + self.path_battery + "/AllowMaxVoltage", + 1 if self.battery.allow_max_voltage else 0, + 0, + 0, + ], + "ClassAndVrmInstance": [ + self.path_battery + "/ClassAndVrmInstance", + class_and_vrm_instance, + 0, + 0, + ], + "CustomName": [ + self.path_battery + "/CustomName", + custom_name, + 0, + 0, + ], + "LastSeen": [ + self.path_battery + "/LastSeen", + int(time()), + 0, + 0, + ], + "MaxVoltageStartTime": [ + self.path_battery + "/MaxVoltageStartTime", + ( + self.battery.max_voltage_start_time + if self.battery.max_voltage_start_time is not None + else "" + ), + 0, + 0, + ], + "SocCalc": [ + self.path_battery + "/SocCalc", + (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, + 0, + 0, + ], + "UniqueIdentifier": [ + self.path_battery + "/UniqueIdentifier", + self.bms_id, 0, 0, ], } - self.settings = SettingsDevice(get_bus(), settings, self.handle_changed_setting) + # update last seen + if found_bms: + self.setSetting( + get_bus(), + "com.victronenergy.settings", + self.path_battery, + "LastSeen", + int(time()), + ) + + self.settings.addSettings(settings) self.battery.role, self.instance = self.get_role_instance() + # create pid file + self.create_pid_file() + + logger.info(f"Used DeviceInstances = {device_instances_used}") + def get_role_instance(self): - val = self.settings["instance"].split(":") + val = self.settings["ClassAndVrmInstance"].split(":") logger.info("DeviceInstance = %d", int(val[1])) return val[0], int(val[1]) def handle_changed_setting(self, setting, oldvalue, newvalue): - if setting == "instance": + if setting == "ClassAndVrmInstance": self.battery.role, self.instance = self.get_role_instance() - logger.info("Changed DeviceInstance = %d", self.instance) + logger.info(f"Changed DeviceInstance = {self.instance}") + return + if setting == "CustomName": + logger.info(f"Changed CustomName = {newvalue}") return + # this function is called when the battery is initiated def setup_vedbus(self): # Set up dbus service and device instance # and notify of all the attributes we intend to update # This is only called once when a battery is initiated self.setup_instance() - short_port = self.battery.port[self.battery.port.rfind("/") + 1 :] - logger.info("%s" % ("com.victronenergy.battery." + short_port)) + logger.info("%s" % (self._dbusname)) # Get the settings for the battery if not self.battery.get_settings(): @@ -100,20 +487,20 @@ def setup_vedbus(self): self._dbusservice.add_path("/Connected", 1) self._dbusservice.add_path( "/CustomName", - self.battery.custom_name(), + self.settings["CustomName"], writeable=True, - onchangecallback=self.battery.custom_name_callback, - ) - self._dbusservice.add_path( - "/Serial", self.battery.unique_identifier(), writeable=True + onchangecallback=self.custom_name_callback, ) + self._dbusservice.add_path("/Serial", self.bms_id, writeable=True) self._dbusservice.add_path( "/DeviceName", self.battery.custom_field, writeable=True ) # Create static battery info self._dbusservice.add_path( - "/Info/BatteryLowVoltage", self.battery.min_battery_voltage, writeable=True + "/Info/BatteryLowVoltage", + self.battery.min_battery_voltage, + writeable=True, ) self._dbusservice.add_path( "/Info/MaxChargeVoltage", @@ -171,6 +558,10 @@ def setup_vedbus(self): # Create SOC, DC and System items self._dbusservice.add_path("/Soc", None, writeable=True) + # add original SOC for comparing + if utils.SOC_CALCULATION: + self._dbusservice.add_path("/SocBms", None, writeable=True) + self._dbusservice.add_path( "/Dc/0/Voltage", None, @@ -239,19 +630,31 @@ def setup_vedbus(self): self._dbusservice.add_path("/Io/AllowToBalance", 0, writeable=True) self._dbusservice.add_path( "/Io/ForceChargingOff", - 0, + ( + 0 + if "force_charging_off_callback" in self.battery.available_callbacks + else None + ), writeable=True, onchangecallback=self.battery.force_charging_off_callback, ) self._dbusservice.add_path( "/Io/ForceDischargingOff", - 0, + ( + 0 + if "force_discharging_off_callback" in self.battery.available_callbacks + else None + ), writeable=True, onchangecallback=self.battery.force_discharging_off_callback, ) self._dbusservice.add_path( "/Io/TurnBalancingOff", - 0, + ( + 0 + if "turn_balancing_off_callback" in self.battery.available_callbacks + else None + ), writeable=True, onchangecallback=self.battery.turn_balancing_off_callback, ) @@ -330,7 +733,7 @@ def setup_vedbus(self): ) logger.info(f"publish config values = {utils.PUBLISH_CONFIG_VALUES}") - if utils.PUBLISH_CONFIG_VALUES == 1: + if utils.PUBLISH_CONFIG_VALUES: publish_config_variables(self._dbusservice) if self.battery.has_settings: @@ -370,8 +773,18 @@ def publish_battery(self, loop): ) # if the battery did not update in 10 second, it's assumed to be offline - if time_since_first_error >= 10: + if time_since_first_error >= 10 and self.battery.online: self.battery.online = False + + # check if the cell voltages are good to go for some minutes + self.cell_voltages_good = ( + True + if self.battery.get_min_cell_voltage() > 3.25 + and self.battery.get_max_cell_voltage() < 3.35 + else False + ) + + # reset the battery values self.battery.init_values() # block charge/discharge @@ -379,7 +792,13 @@ def publish_battery(self, loop): self.block_because_disconnect = True # if the battery did not update in 60 second, it's assumed to be completely failed - if time_since_first_error >= 60: + if time_since_first_error >= 60 and ( + utils.BLOCK_ON_DISCONNECT or not self.cell_voltages_good + ): + loop.quit() + + # if the cells are between 3.2 and 3.3 volt we can continue for some time + if time_since_first_error >= 60 * 20 and not utils.BLOCK_ON_DISCONNECT: loop.quit() # This is to mannage CVCL @@ -398,9 +817,20 @@ 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 + ) + # add original SOC for comparing + self._dbusservice["/SocBms"] = ( + round(self.battery.soc, 2) if self.battery.soc 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 ) @@ -464,13 +894,13 @@ def publish_dbus(self): 0 if self.battery.online else 1 ) self._dbusservice["/System/MinCellTemperature"] = self.battery.get_min_temp() - self._dbusservice[ - "/System/MinTemperatureCellId" - ] = self.battery.get_min_temp_id() + self._dbusservice["/System/MinTemperatureCellId"] = ( + self.battery.get_min_temp_id() + ) self._dbusservice["/System/MaxCellTemperature"] = self.battery.get_max_temp() - self._dbusservice[ - "/System/MaxTemperatureCellId" - ] = self.battery.get_max_temp_id() + self._dbusservice["/System/MaxTemperatureCellId"] = ( + self.battery.get_max_temp_id() + ) self._dbusservice["/System/MOSTemperature"] = self.battery.get_mos_temp() self._dbusservice["/System/Temperature1"] = self.battery.temp1 self._dbusservice["/System/Temperature1Name"] = utils.TEMP_1_NAME @@ -489,37 +919,37 @@ def publish_dbus(self): ) # Charge control - self._dbusservice[ - "/Info/MaxChargeCurrent" - ] = self.battery.control_charge_current - self._dbusservice[ - "/Info/MaxDischargeCurrent" - ] = self.battery.control_discharge_current + self._dbusservice["/Info/MaxChargeCurrent"] = ( + self.battery.control_charge_current + ) + self._dbusservice["/Info/MaxDischargeCurrent"] = ( + self.battery.control_discharge_current + ) # Voltage and charge control info self._dbusservice["/Info/ChargeMode"] = self.battery.charge_mode self._dbusservice["/Info/ChargeModeDebug"] = self.battery.charge_mode_debug self._dbusservice["/Info/ChargeLimitation"] = self.battery.charge_limitation - self._dbusservice[ - "/Info/DischargeLimitation" - ] = self.battery.discharge_limitation + self._dbusservice["/Info/DischargeLimitation"] = ( + self.battery.discharge_limitation + ) # Updates from cells self._dbusservice["/System/MinVoltageCellId"] = self.battery.get_min_cell_desc() self._dbusservice["/System/MaxVoltageCellId"] = self.battery.get_max_cell_desc() - self._dbusservice[ - "/System/MinCellVoltage" - ] = self.battery.get_min_cell_voltage() - self._dbusservice[ - "/System/MaxCellVoltage" - ] = self.battery.get_max_cell_voltage() + self._dbusservice["/System/MinCellVoltage"] = ( + self.battery.get_min_cell_voltage() + ) + self._dbusservice["/System/MaxCellVoltage"] = ( + self.battery.get_max_cell_voltage() + ) self._dbusservice["/Balancing"] = self.battery.get_balancing() # Update the alarms self._dbusservice["/Alarms/LowVoltage"] = self.battery.protection.voltage_low - self._dbusservice[ - "/Alarms/LowCellVoltage" - ] = self.battery.protection.voltage_cell_low + self._dbusservice["/Alarms/LowCellVoltage"] = ( + self.battery.protection.voltage_cell_low + ) # disable high voltage warning temporarly, if loading to bulk voltage and bulk voltage reached is 30 minutes ago self._dbusservice["/Alarms/HighVoltage"] = ( self.battery.protection.voltage_high @@ -530,41 +960,41 @@ def publish_dbus(self): else 0 ) self._dbusservice["/Alarms/LowSoc"] = self.battery.protection.soc_low - self._dbusservice[ - "/Alarms/HighChargeCurrent" - ] = self.battery.protection.current_over - self._dbusservice[ - "/Alarms/HighDischargeCurrent" - ] = self.battery.protection.current_under - self._dbusservice[ - "/Alarms/CellImbalance" - ] = self.battery.protection.cell_imbalance - self._dbusservice[ - "/Alarms/InternalFailure" - ] = self.battery.protection.internal_failure - self._dbusservice[ - "/Alarms/HighChargeTemperature" - ] = self.battery.protection.temp_high_charge - self._dbusservice[ - "/Alarms/LowChargeTemperature" - ] = self.battery.protection.temp_low_charge - self._dbusservice[ - "/Alarms/HighTemperature" - ] = self.battery.protection.temp_high_discharge - self._dbusservice[ - "/Alarms/LowTemperature" - ] = self.battery.protection.temp_low_discharge + self._dbusservice["/Alarms/HighChargeCurrent"] = ( + self.battery.protection.current_over + ) + self._dbusservice["/Alarms/HighDischargeCurrent"] = ( + self.battery.protection.current_under + ) + self._dbusservice["/Alarms/CellImbalance"] = ( + self.battery.protection.cell_imbalance + ) + self._dbusservice["/Alarms/InternalFailure"] = ( + self.battery.protection.internal_failure + ) + self._dbusservice["/Alarms/HighChargeTemperature"] = ( + self.battery.protection.temp_high_charge + ) + self._dbusservice["/Alarms/LowChargeTemperature"] = ( + self.battery.protection.temp_low_charge + ) + self._dbusservice["/Alarms/HighTemperature"] = ( + self.battery.protection.temp_high_discharge + ) + self._dbusservice["/Alarms/LowTemperature"] = ( + self.battery.protection.temp_low_discharge + ) self._dbusservice["/Alarms/BmsCable"] = ( 2 if self.block_because_disconnect else 0 ) - self._dbusservice[ - "/Alarms/HighInternalTemperature" - ] = self.battery.protection.temp_high_internal + self._dbusservice["/Alarms/HighInternalTemperature"] = ( + self.battery.protection.temp_high_internal + ) # cell voltages if utils.BATTERY_CELL_DATA_FORMAT > 0: try: - voltageSum = 0 + voltage_sum = 0 for i in range(self.battery.cell_count): voltage = self.battery.get_cell_voltage(i) cellpath = ( @@ -574,18 +1004,19 @@ def publish_dbus(self): ) self._dbusservice[cellpath % (str(i + 1))] = voltage if utils.BATTERY_CELL_DATA_FORMAT & 1: - self._dbusservice[ - "/Balances/Cell%s" % (str(i + 1)) - ] = self.battery.get_cell_balancing(i) + self._dbusservice["/Balances/Cell%s" % (str(i + 1))] = ( + self.battery.get_cell_balancing(i) + ) if voltage: - voltageSum += voltage + voltage_sum += voltage pathbase = ( "Cell" if (utils.BATTERY_CELL_DATA_FORMAT & 2) else "Voltages" ) - self._dbusservice["/%s/Sum" % pathbase] = voltageSum - self._dbusservice["/%s/Diff" % pathbase] = ( + self._dbusservice["/%s/Sum" % pathbase] = round(voltage_sum, 2) + self._dbusservice["/%s/Diff" % pathbase] = round( self.battery.get_max_cell_voltage() - - self.battery.get_min_cell_voltage() + - self.battery.get_min_cell_voltage(), + 3, ) except Exception: pass @@ -602,20 +1033,6 @@ def publish_dbus(self): if len(self.battery.current_avg_lst) > 300: del self.battery.current_avg_lst[0] - """ - logger.info( - str(self.battery.capacity) - + " - " - + str(utils.TIME_TO_GO_ENABLE) - + " - " - + str(len(utils.TIME_TO_SOC_POINTS)) - + " - " - + str(int(time()) - self.battery.time_to_soc_update) - + " - " - + str(utils.TIME_TO_SOC_RECALCULATE_EVERY) - ) - """ - if ( self.battery.capacity is not None and (utils.TIME_TO_GO_ENABLE or len(utils.TIME_TO_SOC_POINTS) > 0) @@ -634,17 +1051,21 @@ def publish_dbus(self): self._dbusservice["/CurrentAvg"] = self.battery.current_avg - crntPrctPerSec = ( + percent_per_seconds = ( abs(self.battery.current_avg / (self.battery.capacity / 100)) / 3600 ) # Update TimeToGo item - if utils.TIME_TO_GO_ENABLE and crntPrctPerSec is not None: + if utils.TIME_TO_GO_ENABLE and percent_per_seconds is not None: # Update TimeToGo item, has to be a positive int since it's used from dbus-systemcalc-py time_to_go = self.battery.get_timeToSoc( # switch value depending on charging/discharging - utils.SOC_LOW_WARNING if self.battery.current_avg < 0 else 100, - crntPrctPerSec, + ( + utils.SOC_LOW_WARNING + if self.battery.current_avg < 0 + else 100 + ), + percent_per_seconds, True, ) @@ -660,7 +1081,7 @@ def publish_dbus(self): if len(utils.TIME_TO_SOC_POINTS) > 0: for num in utils.TIME_TO_SOC_POINTS: self._dbusservice["/TimeToSoC/" + str(num)] = ( - self.battery.get_timeToSoc(num, crntPrctPerSec) + self.battery.get_timeToSoc(num, percent_per_seconds) if self.battery.current_avg else None ) @@ -674,9 +1095,194 @@ def publish_dbus(self): ) pass + # save settings every 15 seconds to dbus + if int(time()) % 15: + self.saveBatteryOptions() + if self.battery.soc is not None: logger.debug("logged to dbus [%s]" % str(round(self.battery.soc, 2))) self.battery.log_cell_data() if self.battery.has_settings: self._dbusservice["/Settings/ResetSoc"] = self.battery.reset_soc + + def getSettingsWithValues( + self, bus, service: str, object_path: str, recursive: bool = True + ) -> dict: + # print(object_path) + obj = bus.get_object(service, object_path) + iface = dbus.Interface(obj, "org.freedesktop.DBus.Introspectable") + xml_string = iface.Introspect() + # print(xml_string) + result = {} + for child in ElementTree.fromstring(xml_string): + if child.tag == "node" and recursive: + if object_path == "/": + object_path = "" + new_path = "/".join((object_path, child.attrib["name"])) + # result.update(getSettingsWithValues(bus, service, new_path)) + result_sub = self.getSettingsWithValues(bus, service, new_path) + self.merge_dicts(result, result_sub) + elif child.tag == "interface": + if child.attrib["name"] == "com.victronenergy.Settings": + settings_iface = dbus.Interface(obj, "com.victronenergy.BusItem") + method = settings_iface.get_dbus_method("GetValue") + try: + value = method() + if type(value) is not dbus.Dictionary: + # result[object_path] = str(value) + self.merge_dicts( + result, + self.create_nested_dict(object_path, str(value)), + ) + # print(f"{object_path}: {value}") + if not recursive: + return value + except dbus.exceptions.DBusException as e: + logger.error( + f"getSettingsWithValues(): Failed to get value: {e}" + ) + + return result + + def setSetting( + self, bus, service: str, object_path: str, setting_name: str, value + ) -> bool: + obj = bus.get_object(service, object_path + "/" + setting_name) + # iface = dbus.Interface(obj, "org.freedesktop.DBus.Introspectable") + # xml_string = iface.Introspect() + # print(xml_string) + settings_iface = dbus.Interface(obj, "com.victronenergy.BusItem") + method = settings_iface.get_dbus_method("SetValue") + try: + logger.debug(f"Setted setting {object_path}/{setting_name} to {value}") + return True if method(value) == 0 else False + except dbus.exceptions.DBusException as e: + logger.error(f"Failed to set setting: {e}") + + def removeSetting( + self, bus, service: str, object_path: str, setting_name: list + ) -> bool: + obj = bus.get_object(service, object_path) + # iface = dbus.Interface(obj, "org.freedesktop.DBus.Introspectable") + # xml_string = iface.Introspect() + # print(xml_string) + settings_iface = dbus.Interface(obj, "com.victronenergy.Settings") + method = settings_iface.get_dbus_method("RemoveSettings") + try: + logger.debug(f"Removed setting at {object_path}") + return True if method(setting_name) == 0 else False + except dbus.exceptions.DBusException as e: + logger.error(f"Failed to remove setting: {e}") + + def create_nested_dict(self, path, value) -> dict: + keys = path.strip("/").split("/") + result = current = {} + for key in keys[:-1]: + current[key] = {} + current = current[key] + current[keys[-1]] = value + return result + + def merge_dicts(self, dict1, dict2) -> None: + for key in dict2: + if ( + key in dict1 + and isinstance(dict1[key], dict) + and isinstance(dict2[key], dict) + ): + self.merge_dicts(dict1[key], dict2[key]) + else: + dict1[key] = dict2[key] + + # save custom name to dbus + def custom_name_callback(self, path, value) -> str: + result = self.setSetting( + get_bus(), + "com.victronenergy.settings", + self.path_battery, + "CustomName", + value, + ) + logger.debug( + f'CustomName changed to "{value}" for {self.path_battery}: {result}' + ) + return value if result else None + + # save battery options to dbus + def saveBatteryOptions(self) -> bool: + result = True + + if ( + self.battery.allow_max_voltage + != self.save_charge_details_last["allow_max_voltage"] + ): + result = result + self.setSetting( + get_bus(), + "com.victronenergy.settings", + self.path_battery, + "AllowMaxVoltage", + 1 if self.battery.allow_max_voltage else 0, + ) + logger.debug( + f"Saved AllowMaxVoltage. Before {self.save_charge_details_last['allow_max_voltage']}, " + + f"after {self.battery.allow_max_voltage}" + ) + self.save_charge_details_last["allow_max_voltage"] = ( + self.battery.allow_max_voltage + ) + + if ( + self.battery.max_voltage_start_time + != self.save_charge_details_last["max_voltage_start_time"] + ): + result = result and self.setSetting( + get_bus(), + "com.victronenergy.settings", + self.path_battery, + "MaxVoltageStartTime", + ( + self.battery.max_voltage_start_time + if self.battery.max_voltage_start_time is not None + else "" + ), + ) + logger.debug( + f"Saved MaxVoltageStartTime. Before {self.save_charge_details_last['max_voltage_start_time']}, " + + f"after {self.battery.max_voltage_start_time}" + ) + self.save_charge_details_last["max_voltage_start_time"] = ( + self.battery.max_voltage_start_time + ) + + if self.battery.soc_calc != self.save_charge_details_last["soc_calc"]: + result = result and self.setSetting( + get_bus(), + "com.victronenergy.settings", + self.path_battery, + "SocCalc", + self.battery.soc_calc, + ) + logger.debug(f"soc_calc written to dbus: {self.battery.soc_calc}") + self.save_charge_details_last["soc_calc"] = self.battery.soc_calc + + if ( + self.battery.soc_reset_last_reached + != self.save_charge_details_last["soc_reset_last_reached"] + ): + result = result and self.setSetting( + get_bus(), + "com.victronenergy.settings", + self.path_battery, + "SocResetLastReached", + self.battery.soc_reset_last_reached, + ) + logger.debug( + f"Saved SocResetLastReached. Before {self.save_charge_details_last['soc_reset_last_reached']}, " + + f"after {self.battery.soc_reset_last_reached}", + ) + self.save_charge_details_last["soc_reset_last_reached"] = ( + self.battery.soc_reset_last_reached + ) + + return result diff --git a/etc/dbus-serialbattery/install-qml.sh b/etc/dbus-serialbattery/install-qml.sh index 287aac83..4ff23d3a 100755 --- a/etc/dbus-serialbattery/install-qml.sh +++ b/etc/dbus-serialbattery/install-qml.sh @@ -48,18 +48,51 @@ fi if [ ! -f /opt/victronenergy/gui/qml/PageLynxIonIo.qml.backup ]; then cp /opt/victronenergy/gui/qml/PageLynxIonIo.qml /opt/victronenergy/gui/qml/PageLynxIonIo.qml.backup fi -# copy new PageBattery.qml -cp /data/etc/dbus-serialbattery/qml/PageBattery.qml /opt/victronenergy/gui/qml/ -# copy new PageBatteryCellVoltages -cp /data/etc/dbus-serialbattery/qml/PageBatteryCellVoltages.qml /opt/victronenergy/gui/qml/ -# copy new PageBatteryParameters.qml -cp /data/etc/dbus-serialbattery/qml/PageBatteryParameters.qml /opt/victronenergy/gui/qml/ -# copy new PageBatterySettings.qml -cp /data/etc/dbus-serialbattery/qml/PageBatterySettings.qml /opt/victronenergy/gui/qml/ -# copy new PageBatterySetup -cp /data/etc/dbus-serialbattery/qml/PageBatterySetup.qml /opt/victronenergy/gui/qml/ -# copy new PageLynxIonIo.qml -cp /data/etc/dbus-serialbattery/qml/PageLynxIonIo.qml /opt/victronenergy/gui/qml/ + +# count changed files +filesChanged=0 + +# copy new PageBattery.qml if changed +if ! cmp -s /data/etc/dbus-serialbattery/qml/PageBattery.qml /opt/victronenergy/gui/qml/PageBattery.qml +then + cp /data/etc/dbus-serialbattery/qml/PageBattery.qml /opt/victronenergy/gui/qml/ + ((filesChanged++)) +fi + +# copy new PageBatteryCellVoltages if changed +if ! cmp -s /data/etc/dbus-serialbattery/qml/PageBatteryCellVoltages.qml /opt/victronenergy/gui/qml/PageBatteryCellVoltages.qml +then + cp /data/etc/dbus-serialbattery/qml/PageBatteryCellVoltages.qml /opt/victronenergy/gui/qml/ + ((filesChanged++)) +fi + +# copy new PageBatteryParameters.qml if changed +if ! cmp -s /data/etc/dbus-serialbattery/qml/PageBatteryParameters.qml /opt/victronenergy/gui/qml/PageBatteryParameters.qml +then + cp /data/etc/dbus-serialbattery/qml/PageBatteryParameters.qml /opt/victronenergy/gui/qml/ + ((filesChanged++)) +fi + +# copy new PageBatterySettings.qml if changed +if ! cmp -s /data/etc/dbus-serialbattery/qml/PageBatterySettings.qml /opt/victronenergy/gui/qml/PageBatterySettings.qml +then + cp /data/etc/dbus-serialbattery/qml/PageBatterySettings.qml /opt/victronenergy/gui/qml/ + ((filesChanged++)) +fi + +# copy new PageBatterySetup if changed +if ! cmp -s /data/etc/dbus-serialbattery/qml/PageBatterySetup.qml /opt/victronenergy/gui/qml/PageBatterySetup.qml +then + cp /data/etc/dbus-serialbattery/qml/PageBatterySetup.qml /opt/victronenergy/gui/qml/ + ((filesChanged++)) +fi + +# copy new PageLynxIonIo.qml if changed +if ! cmp -s /data/etc/dbus-serialbattery/qml/PageLynxIonIo.qml /opt/victronenergy/gui/qml/PageLynxIonIo.qml +then + cp /data/etc/dbus-serialbattery/qml/PageLynxIonIo.qml /opt/victronenergy/gui/qml/ + ((filesChanged++)) +fi # get current Venus OS version @@ -86,10 +119,13 @@ if (( $venusVersionNumber < $versionNumber )); then echo "done." fi - -# stop gui -svc -d /service/gui -# sleep 1 sec -sleep 1 -# start gui -svc -u /service/gui +# if files changed, restart gui +if [ $filesChanged -gt 0 ]; then + # stop gui + svc -d /service/gui + # sleep 1 sec + sleep 1 + # start gui + svc -u /service/gui + echo "New QML files were installed and the GUI was restarted." +fi diff --git a/etc/dbus-serialbattery/install.sh b/etc/dbus-serialbattery/install.sh index d0fbbde8..df80f7e4 100644 --- a/etc/dbus-serialbattery/install.sh +++ b/etc/dbus-serialbattery/install.sh @@ -4,6 +4,20 @@ #set -x +# check if at least 5 MB free space is available on the system partition +freeSpace=$(df -m /data | awk 'NR==2 {print $4}') +if [ $freeSpace -lt 5 ]; then + echo + echo + echo "ERROR: Not enough free space on the data partition. At least 5 MB are required." + echo + echo " Please free up some space and try again." + echo + echo + exit 1 +fi + + echo # fetch version numbers for different versions echo -n "Fetch current version numbers..." @@ -14,8 +28,8 @@ latest_release_louisvdw_stable=$(curl -s https://api.github.com/repos/Louisvdw/d # louisvdw beta latest_release_louisvdw_beta=$(curl -s https://api.github.com/repos/Louisvdw/dbus-serialbattery/releases | grep "tag_name.*beta" | cut -d : -f 2,3 | tr -d "\ " | tr -d \" | tr -d \, | head -n 1) -# louisvdw dev -latest_release_louisvdw_dev=$(curl -s https://raw.githubusercontent.com/Louisvdw/dbus-serialbattery/dev/etc/dbus-serialbattery/utils.py | grep DRIVER_VERSION | awk -F'"' '{print "v" $2}') +# louisvdw master branch +latest_release_louisvdw_nightly=$(curl -s https://raw.githubusercontent.com/Louisvdw/dbus-serialbattery/master/etc/dbus-serialbattery/utils.py | grep DRIVER_VERSION | awk -F'"' '{print "v" $2}') # mr-manuel stable latest_release_mrmanuel_stable=$(curl -s https://api.github.com/repos/mr-manuel/venus-os_dbus-serialbattery/releases/latest | grep "tag_name" | cut -d : -f 2,3 | tr -d "\ " | tr -d \" | tr -d \,) @@ -23,8 +37,8 @@ latest_release_mrmanuel_stable=$(curl -s https://api.github.com/repos/mr-manuel/ # mr-manuel beta latest_release_mrmanuel_beta=$(curl -s https://api.github.com/repos/mr-manuel/venus-os_dbus-serialbattery/releases | grep "tag_name.*beta" | cut -d : -f 2,3 | tr -d "\ " | tr -d \" | tr -d \, | head -n 1) -# mr-manuel dev -latest_release_mrmanuel_dev=$(curl -s https://raw.githubusercontent.com/mr-manuel/venus-os_dbus-serialbattery/dev/etc/dbus-serialbattery/utils.py | grep DRIVER_VERSION | awk -F'"' '{print "v" $2}') +# mr-manuel master branch +latest_release_mrmanuel_nightly=$(curl -s https://raw.githubusercontent.com/mr-manuel/venus-os_dbus-serialbattery/master/etc/dbus-serialbattery/utils.py | grep DRIVER_VERSION | awk -F'"' '{print "v" $2}') # done echo " done." @@ -40,8 +54,8 @@ version_list=( "latest release \"$latest_release_mrmanuel_stable\" (mr-manuel's repo, stable, most up to date)" "beta build \"$latest_release_louisvdw_beta\" (louisvdw's repo)" "beta build \"$latest_release_mrmanuel_beta\" (mr-manuel's repo, no errors after 72 h runtime, long time testing needed)" - "dev build \"$latest_release_louisvdw_dev\" (louisvdw's repo)" - "dev build \"$latest_release_mrmanuel_dev\" (mr-manuel's repo, newest features and fixes, bugs possible)" + "nightly build \"$latest_release_louisvdw_nightly\" (louisvdw's repo)" + "nightly build \"$latest_release_mrmanuel_nightly\" (mr-manuel's repo, newest features and fixes, bugs possible)" "specific version" "local tar file" "quit" @@ -70,12 +84,12 @@ do #echo "Selected number: $REPLY" break ;; - "dev build \"$latest_release_louisvdw_dev\" (louisvdw's repo)") + "nightly build \"$latest_release_louisvdw_nightly\" (louisvdw's repo)") echo "Selected: $version" #echo "Selected number: $REPLY" break ;; - "dev build \"$latest_release_mrmanuel_dev\" (mr-manuel's repo, newest features and fixes, bugs possible)") + "nightly build \"$latest_release_mrmanuel_nightly\" (mr-manuel's repo, newest features and fixes, bugs possible)") echo "Selected: $version" #echo "Selected number: $REPLY" break @@ -171,14 +185,14 @@ fi -## dev builds -if [ "$version" = "dev build \"$latest_release_louisvdw_dev\" (louisvdw's repo)" ] || [ "$version" = "dev build \"$latest_release_mrmanuel_dev\" (mr-manuel's repo, newest features and fixes, bugs possible)" ]; then +## nightly builds +if [ "$version" = "nightly build \"$latest_release_louisvdw_nightly\" (louisvdw's repo)" ] || [ "$version" = "nightly build \"$latest_release_mrmanuel_nightly\" (mr-manuel's repo, newest features and fixes, bugs possible)" ]; then - branch="dev" + branch="master" cd /tmp - if [ "$version" = "dev build \"$latest_release_mrmanuel_dev\" (mr-manuel's repo, newest features and fixes, bugs possible)" ]; then + if [ "$version" = "nightly build \"$latest_release_mrmanuel_nightly\" (mr-manuel's repo, newest features and fixes, bugs possible)" ]; then # clean already extracted folder rm -rf /tmp/venus-os_dbus-serialbattery-$branch diff --git a/etc/dbus-serialbattery/reinstall-local.sh b/etc/dbus-serialbattery/reinstall-local.sh index ffb4b25a..f12b83c1 100755 --- a/etc/dbus-serialbattery/reinstall-local.sh +++ b/etc/dbus-serialbattery/reinstall-local.sh @@ -60,6 +60,38 @@ fi # check if minimum required Venus OS is installed | end +# check if at least 8 MB free space is available on the system partition +freeSpace=$(df -m / | awk 'NR==2 {print $4}') +if [ $freeSpace -lt 8 ]; then + + # try to expand system partition + bash /opt/victronenergy/swupdate-scripts/resize2fs.sh + + freeSpace=$(df -m / | awk 'NR==2 {print $4}') + if [ $freeSpace -lt 8 ]; then + echo + echo + echo "ERROR: Not enough free space on the system partition. At least 8 MB are required." + echo + echo " Please please try to execute this command" + echo + echo " bash /opt/victronenergy/swupdate-scripts/resize2fs.sh" + echo + echo " and try the installation again after." + echo + echo + exit 1 + else + echo + echo + echo "INFO: System partition was expanded. Now there are $freeSpace MB free space available." + echo + echo + fi + +fi + + # handle read only mounts bash /opt/victronenergy/swupdate-scripts/remount-rw.sh @@ -131,6 +163,25 @@ pkill -f "python .*/dbus-serialbattery.py /dev/tty.*" ### BLUETOOTH PART | START ### +# get bluetooth mode integrated/usb +bluetooth_use_usb=$(awk -F "=" '/^BLUETOOTH_USE_USB/ {print $2}' /data/etc/dbus-serialbattery/config.ini) + +# works only for Raspberry Pi, since GX devices don't have a /u-boot/config.txt +# replace dtoverlay in /u-boot/config.txt this needs a reboot! +if [ -f "/u-boot/config.txt" ]; then + if [[ $bluetooth_use_usb == *"True"* ]]; then + if grep -q -r "miniuart-bt" /u-boot/config.txt; then + sed -i 's/miniuart-bt/disable-bt/g' /u-boot/config.txt + echo "ATTENTION! You have changed the bluetooth mode to USB! THIS NEEDS A MANUAL REBOOT!" + fi + elif [[ $bluetooth_use_usb == *"False"* ]]; then + if grep -q -r "disable-bt" /u-boot/config.txt; then + sed -i 's/disable-bt/miniuart-bt/g' /u-boot/config.txt + echo "ATTENTION! You have changed the bluetooth mode to built in module! THIS NEEDS A MANUAL REBOOT!" + fi + fi +fi + # get BMS list from config file bluetooth_bms=$(awk -F "=" '/^BLUETOOTH_BMS/ {print $2}' /data/etc/dbus-serialbattery/config.ini) #echo $bluetooth_bms @@ -149,7 +200,7 @@ bluetooth_length=${#bms_array[@]} # stop all dbus-blebattery services, if at least one exists if [ -d "/service/dbus-blebattery.0" ]; then - svc -u /service/dbus-blebattery.* + svc -t /service/dbus-blebattery.* # always remove existing blebattery services to cleanup rm -rf /service/dbus-blebattery.* @@ -174,8 +225,8 @@ if [ "$bluetooth_length" -gt 0 ]; then echo # install required packages - # TO DO: Check first if packages are already installed - echo "Installing required packages to use Bluetooth connection..." + echo "Checking required packages to use Bluetooth connection..." + echo # dbus-fast: skip compiling/building the wheel # else weak system crash and are not able to install it, @@ -183,11 +234,30 @@ if [ "$bluetooth_length" -gt 0 ]; then # and https://github.com/Louisvdw/dbus-serialbattery/issues/785 export SKIP_CYTHON=false - opkg update - opkg install python3-misc python3-pip + if ! opkg list-installed | grep -q "python3-misc" || ! opkg list-installed | grep -q "python3-pip"; then + echo "Update packages..." + opkg update + echo + fi + + if ! opkg list-installed | grep -q "python3-misc"; then + echo "Install python3-misc..." + opkg install python3-misc + echo + fi - echo - pip3 install bleak + if ! opkg list-installed | grep -q "python3-pip"; then + echo "Install python3-pip..." + opkg install python3-pip + echo + fi + + # fastest way to check if bleak is installed + if [ ! -f "/usr/lib/python3.8/site-packages/bleak/__init__.py" ]; then + echo "Install bleak..." + pip3 install bleak + echo + fi # # ONLY FOR TESTING if there are version issues # echo @@ -317,7 +387,7 @@ can_lenght=${#can_array[@]} # stop all dbus-canbattery services, if at least one exists if [ -d "/service/dbus-canbattery.0" ]; then - svc -u /service/dbus-canbattery.* + svc -t /service/dbus-canbattery.* # always remove existing canbattery services to cleanup rm -rf /service/dbus-canbattery.* @@ -339,14 +409,33 @@ if [ "$can_lenght" -gt 0 ]; then echo # install required packages - # TO DO: Check first if packages are already installed - echo "Installing required packages to use CAN connection..." + echo "Checking required packages to use CAN connection..." + echo + + if ! opkg list-installed | grep -q "python3-misc" || ! opkg list-installed | grep -q "python3-pip"; then + echo "Update packages..." + opkg update + echo + fi - opkg update - opkg install python3-misc python3-pip + if ! opkg list-installed | grep -q "python3-misc"; then + echo "Install python3-misc..." + opkg install python3-misc + echo + fi - echo - pip3 install python-can + if ! opkg list-installed | grep -q "python3-pip"; then + echo "Install python3-pip..." + opkg install python3-pip + echo + fi + + # fastest way to check if can is installed + if [ ! -f "/usr/lib/python3.8/site-packages/can/__init__.py" ]; then + echo "Install can..." + pip3 install can + echo + fi echo "done." echo @@ -458,3 +547,13 @@ echo "CUSTOM SETTINGS: If you want to add custom settings, then check the settin echo " and add them to \"/data/etc/dbus-serialbattery/config.ini\" to persist future driver updates." echo echo +echo "GUIv2: If you want to try the new GUIv2 follow this link:" +echo " https://github.com/mr-manuel/venus-os_dbus-serialbattery/tree/dev/gui-v2" +echo +echo +# print which version was installed +# fetch line 40 from utils.py +line=$(cat /data/etc/dbus-serialbattery/utils.py | grep DRIVER_VERSION | awk -F'"' '{print "v" $2}') +echo "*** dbus-serialbattery $line was installed. ***" +echo +echo diff --git a/etc/dbus-serialbattery/utils.py b/etc/dbus-serialbattery/utils.py index df4ce2ab..7bff655f 100644 --- a/etc/dbus-serialbattery/utils.py +++ b/etc/dbus-serialbattery/utils.py @@ -37,7 +37,7 @@ def _get_list_from_config( # Constants -DRIVER_VERSION = "1.0.20231117dev" +DRIVER_VERSION = "1.2.20240227dev" zero_char = chr(48) degree_sign = "\N{DEGREE SIGN}" @@ -102,6 +102,9 @@ def _get_list_from_config( CELL_VOLTAGE_DIFF_KEEP_MAX_VOLTAGE_UNTIL = float( config["DEFAULT"]["CELL_VOLTAGE_DIFF_KEEP_MAX_VOLTAGE_UNTIL"] ) +CELL_VOLTAGE_DIFF_KEEP_MAX_VOLTAGE_TIME_RESTART = float( + config["DEFAULT"]["CELL_VOLTAGE_DIFF_KEEP_MAX_VOLTAGE_TIME_RESTART"] +) CELL_VOLTAGE_DIFF_TO_RESET_VOLTAGE_LIMIT = float( config["DEFAULT"]["CELL_VOLTAGE_DIFF_TO_RESET_VOLTAGE_LIMIT"] ) @@ -139,12 +142,17 @@ def _get_list_from_config( lambda v: MAX_BATTERY_DISCHARGE_CURRENT * float(v), ) +# --------- Cell Voltage limitation (affecting CVL) --------- + +CVL_ICONTROLLER_MODE = "True" == config["DEFAULT"]["CVL_ICONTROLLER_MODE"] +CVL_ICONTROLLER_FACTOR = float(config["DEFAULT"]["CVL_ICONTROLLER_FACTOR"]) + # --------- Temperature limitation (affecting CCL/DCL) --------- CCCM_T_ENABLE = "True" == config["DEFAULT"]["CCCM_T_ENABLE"] DCCM_T_ENABLE = "True" == config["DEFAULT"]["DCCM_T_ENABLE"] -TEMPERATURE_LIMITS_WHILE_CHARGING = _get_list_from_config( - "DEFAULT", "TEMPERATURE_LIMITS_WHILE_CHARGING", lambda v: float(v) +TEMPERATURES_WHILE_CHARGING = _get_list_from_config( + "DEFAULT", "TEMPERATURES_WHILE_CHARGING", lambda v: float(v) ) MAX_CHARGE_CURRENT_T = _get_list_from_config( "DEFAULT", @@ -152,8 +160,8 @@ def _get_list_from_config( lambda v: MAX_BATTERY_CHARGE_CURRENT * float(v), ) -TEMPERATURE_LIMITS_WHILE_DISCHARGING = _get_list_from_config( - "DEFAULT", "TEMPERATURE_LIMITS_WHILE_DISCHARGING", lambda v: float(v) +TEMPERATURES_WHILE_DISCHARGING = _get_list_from_config( + "DEFAULT", "TEMPERATURES_WHILE_DISCHARGING", lambda v: float(v) ) MAX_DISCHARGE_CURRENT_T = _get_list_from_config( "DEFAULT", @@ -165,32 +173,23 @@ def _get_list_from_config( CCCM_SOC_ENABLE = "True" == config["DEFAULT"]["CCCM_SOC_ENABLE"] DCCM_SOC_ENABLE = "True" == config["DEFAULT"]["DCCM_SOC_ENABLE"] -CC_SOC_LIMIT1 = float(config["DEFAULT"]["CC_SOC_LIMIT1"]) -CC_SOC_LIMIT2 = float(config["DEFAULT"]["CC_SOC_LIMIT2"]) -CC_SOC_LIMIT3 = float(config["DEFAULT"]["CC_SOC_LIMIT3"]) -CC_CURRENT_LIMIT1 = MAX_BATTERY_CHARGE_CURRENT * float( - config["DEFAULT"]["CC_CURRENT_LIMIT1_FRACTION"] +SOC_WHILE_CHARGING = _get_list_from_config( + "DEFAULT", "SOC_WHILE_CHARGING", lambda v: float(v) ) -CC_CURRENT_LIMIT2 = MAX_BATTERY_CHARGE_CURRENT * float( - config["DEFAULT"]["CC_CURRENT_LIMIT2_FRACTION"] -) -CC_CURRENT_LIMIT3 = MAX_BATTERY_CHARGE_CURRENT * float( - config["DEFAULT"]["CC_CURRENT_LIMIT3_FRACTION"] +MAX_CHARGE_CURRENT_SOC = _get_list_from_config( + "DEFAULT", + "MAX_CHARGE_CURRENT_SOC_FRACTION", + lambda v: MAX_BATTERY_CHARGE_CURRENT * float(v), ) -DC_SOC_LIMIT1 = float(config["DEFAULT"]["DC_SOC_LIMIT1"]) -DC_SOC_LIMIT2 = float(config["DEFAULT"]["DC_SOC_LIMIT2"]) -DC_SOC_LIMIT3 = float(config["DEFAULT"]["DC_SOC_LIMIT3"]) - -DC_CURRENT_LIMIT1 = MAX_BATTERY_DISCHARGE_CURRENT * float( - config["DEFAULT"]["DC_CURRENT_LIMIT1_FRACTION"] -) -DC_CURRENT_LIMIT2 = MAX_BATTERY_DISCHARGE_CURRENT * float( - config["DEFAULT"]["DC_CURRENT_LIMIT2_FRACTION"] +SOC_WHILE_DISCHARGING = _get_list_from_config( + "DEFAULT", "SOC_WHILE_DISCHARGING", lambda v: float(v) ) -DC_CURRENT_LIMIT3 = MAX_BATTERY_DISCHARGE_CURRENT * float( - config["DEFAULT"]["DC_CURRENT_LIMIT3_FRACTION"] +MAX_DISCHARGE_CURRENT_SOC = _get_list_from_config( + "DEFAULT", + "MAX_DISCHARGE_CURRENT_SOC_FRACTION", + lambda v: MAX_BATTERY_DISCHARGE_CURRENT * float(v), ) # --------- Time-To-Go --------- @@ -208,6 +207,17 @@ def _get_list_from_config( ) TIME_TO_SOC_INC_FROM = "True" == config["DEFAULT"]["TIME_TO_SOC_INC_FROM"] +# --------- SOC calculation --------- +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_REPORTED_BY_BMS = _get_list_from_config( + "DEFAULT", "SOC_CALC_CURRENT_REPORTED_BY_BMS", lambda v: float(v) +) +SOC_CALC_CURRENT_MEASURED_BY_USER = _get_list_from_config( + "DEFAULT", "SOC_CALC_CURRENT_MEASURED_BY_USER", lambda v: float(v) +) + # --------- Additional settings --------- BMS_TYPE = _get_list_from_config("DEFAULT", "BMS_TYPE", lambda v: str(v)) @@ -215,16 +225,11 @@ def _get_list_from_config( "DEFAULT", "EXCLUDED_DEVICES", lambda v: str(v) ) -CUSTOM_BATTERY_NAMES = _get_list_from_config( - "DEFAULT", "CUSTOM_BATTERY_NAMES", lambda v: str(v) -) - # Auto reset SoC -# If on, then SoC is reset to 100%, if the value switches from absorption to float voltage -# Currently only working for Daly BMS and JK BMS BLE AUTO_RESET_SOC = "True" == config["DEFAULT"]["AUTO_RESET_SOC"] -PUBLISH_CONFIG_VALUES = int(config["DEFAULT"]["PUBLISH_CONFIG_VALUES"]) +# Publish the config settings to the dbus path "/Info/Config/" +PUBLISH_CONFIG_VALUES = "True" == config["DEFAULT"]["PUBLISH_CONFIG_VALUES"] BATTERY_CELL_DATA_FORMAT = int(config["DEFAULT"]["BATTERY_CELL_DATA_FORMAT"]) @@ -434,9 +439,6 @@ def read_serialport_data( return False -locals_copy = locals().copy() - - # Publish config variables to dbus def publish_config_variables(dbusservice): for variable, value in locals_copy.items(): @@ -449,3 +451,6 @@ def publish_config_variables(dbusservice): or isinstance(value, List) ): dbusservice.add_path(f"/Info/Config/{variable}", value) + + +locals_copy = locals().copy() diff --git a/gui-v2/README.md b/gui-v2/README.md new file mode 100644 index 00000000..2f139fbc --- /dev/null +++ b/gui-v2/README.md @@ -0,0 +1,18 @@ +# Venus OS GUIv2 - Alpha + +## 🚨 NOTE 🚨 + +* Currently you can only access through a browser to this GUI +* Many functions are not yet visible in the new GUI, also it is not sure that this is the way it will be implemented in future +* Do not open an issue in the GitHub repository for any problem or feedback. It will be directly closed +* Check [Venus OS: modifying gui-v2](https://community.victronenergy.com/questions/245056/venus-os-modifying-gui-v2.html) for more informations + +## Install + +```bash +wget -O - https://raw.githubusercontent.com/mr-manuel/venus-os_dbus-serialbattery/dev/gui-v2/install-new-webgui.sh | bash +``` + +## How to open + +Open your browser and navigate to [http://venusos/gui-battery](http://venusos/gui-battery). diff --git a/gui-v2/install-new-webgui.sh b/gui-v2/install-new-webgui.sh new file mode 100644 index 00000000..6a12c97a --- /dev/null +++ b/gui-v2/install-new-webgui.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# change to /tmp directory +cd /tmp + +# download the webassembly.zip file +wget -O /tmp/venus-webassembly.zip https://raw.githubusercontent.com/mr-manuel/venus-os_dbus-serialbattery/dev/gui-v2/venus-webassembly.zip + +# unzip the file +unzip /tmp/venus-webassembly.zip + +# remove the old webassembly directory +rm -rf /var/www/venus/gui-battery + +# move the new webassembly directory to the correct location +mv /tmp/wasm /var/www/venus/gui-battery + +echo +echo "The GUIv2 with the dbus-serialbattery mods was installed successfully." +echo +echo "Please check https://github.com/mr-manuel/venus-os_dbus-serialbattery/tree/dev/gui-v2 for more details." +echo diff --git a/gui-v2/venus-webassembly.zip b/gui-v2/venus-webassembly.zip new file mode 100644 index 00000000..1676c24f Binary files /dev/null and b/gui-v2/venus-webassembly.zip differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..8bb6ee5f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.black] +line-length = 88