diff --git a/.flake8 b/.flake8 index 028e4537..5373f0c2 100644 --- a/.flake8 +++ b/.flake8 @@ -2,7 +2,6 @@ max-line-length = 120 exclude = ./etc/dbus-serialbattery/ant.py, - ./etc/dbus-serialbattery/battery.py, ./etc/dbus-serialbattery/battery_template.py, ./etc/dbus-serialbattery/daly.py, ./etc/dbus-serialbattery/dbus-serialbattery.py, @@ -17,7 +16,8 @@ exclude = ./etc/dbus-serialbattery/sinowealth.py, ./etc/dbus-serialbattery/test_max17853.py, ./etc/dbus-serialbattery/util_max17853.py, + ./velib_python venv extend-ignore: - # E203 whitespace fefore ':' conflicts with black code formatting. Will be ignored in flake8 + # E203 whitespace before ':' conflicts with black code formatting. Will be ignored in flake8 E203 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..bf6a83b3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto eol=lf +*.{sh,[sS][hH]} text eol=lf +*.{pyb} text eol=lf +*.{cmd,[cC][mM][dD]} text eol=crlf +*.{bat,[bB][aA][tT]} text eol=crlf diff --git a/.github/workflows/analyse.yml b/.github/workflows/analyse.yml index b1bacf16..8d027e13 100644 --- a/.github/workflows/analyse.yml +++ b/.github/workflows/analyse.yml @@ -6,34 +6,28 @@ on: - '**' jobs: - CodeQL: + analyse: name: Analyze Using GitHub CodeQL runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v3 - + - name: Set up Python environment + uses: actions/setup-python@v4 + with: + python-version: "3.8.13" + - name: Clone velib_python and add it to PYTHONPATH for subsequent steps + run: | + git clone https://github.com/victronenergy/velib_python.git + echo PYTHONPATH=$PYTHONPATH:$(pwd)/velib_python >> $GITHUB_ENV - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: python - - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 - lint: - name: Check Code Formatting - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Execute black lint check - uses: psf/black@stable - - - name: Set up Python environment - uses: actions/setup-python@v4 - with: - python-version: "3.11" - - name: flake8 Lint - uses: py-actions/flake8@v2 + - name: Execute black lint check + uses: psf/black@stable + - name: flake8 Lint + uses: py-actions/flake8@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..c54513bb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,31 @@ +name: Release + +on: + push: + tags: + - "v*.*.*" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: build release archive + run: | + find . -type f -name "*.py" -exec chmod +x {} \; + find . -type f -name "*.sh" -exec chmod +x {} \; + find . -type f -name "run" -exec chmod +x {} \; + tar -czvf venus-data.tar.gz \ + --mode='a+rwX' \ + --exclude battery_template.py \ + --exclude __pycache__ \ + --exclude restartservice.sh \ + --exclude revov.py \ + --exclude test_max17853.py \ + conf/serial-starter.d \ + etc/dbus-serialbattery/ + - name: Release + uses: softprops/action-gh-release@v1 + with: + files: venus-data.tar.gz diff --git a/.gitignore b/.gitignore index 83a2779d..287a9c8b 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,7 @@ BMS-trials .project .pydevproject *.prefs +etc/dbus-serialbattery/config.ini + +# Local Clone of velib_python +velib_python \ No newline at end of file diff --git a/README.md b/README.md index c9e744a1..36bd7163 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,24 @@ The driver will communicate with a Battery Management System (BMS) that support If you find this driver helpful please considder supporting this project. You can buy me a Ko-Fi or get in contact if you would like to donate hardware. [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/Z8Z73LCW1) or using [Paypal.me](https://paypal.me/innernet) + +### Developer Remarks +To develop this project, install the requirements. This project makes use of velib_python which is pre-installed on +Venus-OS Devices under `/opt/victronenergy/dbus-systemcalc-py/ext/velib_python`. To use the python files locally, +`git clone` the [velib_python](https://github.com/victronenergy/velib_python) project to velib_python and add +velib_python to the `PYTHONPATH` environment variable. + +#### How it works +* Each supported BMS needs to implement the abstract base class `Battery` from `battery.py`. +* `dbus-serialbattery.py` tries to figure out the correct connected BMS by looping through all known implementations of +`Battery` and executing its `test_connection()`. If this returns true, `dbus-serialbattery.py` sticks with this battery +and then periodically executes `dbushelpert.publish_battery()`. `publish_battery()` executes `Battery.refresh_data()` which +updates the fields of Battery. It then publishes those fields to dbus using `dbushelper.publish_dbus()` +* The Victron Device will be "controlled" by the values published on `/Info/` - namely: + * `/Info/MaxChargeCurrent ` + * `/Info/MaxDischargeCurrent` + * `/Info/MaxChargeVoltage` + * `/Info/BatteryLowVoltage` + * `/Info/ChargeRequest` (not implemented in dbus-serialbattery) + +For more details on the victron dbus interface see [the official victron dbus documentation](https://github.com/victronenergy/venus/wiki/dbus) diff --git a/buildfiles.lst b/buildfiles.lst deleted file mode 100644 index fe7809b0..00000000 --- a/buildfiles.lst +++ /dev/null @@ -1,32 +0,0 @@ -conf/serial-starter.d -etc/dbus-serialbattery/service/run -etc/dbus-serialbattery/service/log/run -etc/dbus-serialbattery/LICENSE -etc/dbus-serialbattery/README.md -etc/dbus-serialbattery/start-serialbattery.sh -etc/dbus-serialbattery/disabledriver.sh -etc/dbus-serialbattery/installlocal.sh -etc/dbus-serialbattery/installrelease.sh -etc/dbus-serialbattery/reinstalllocal.sh -etc/dbus-serialbattery/restoregui.sh -etc/dbus-serialbattery/installqml.sh -etc/dbus-serialbattery/qml/PageBattery.qml -etc/dbus-serialbattery/qml/PageBatteryCellVoltages.qml -etc/dbus-serialbattery/qml/PageBatterySetup.qml -etc/dbus-serialbattery/minimalmodbus.py -etc/dbus-serialbattery/dbus-serialbattery.py -etc/dbus-serialbattery/dbushelper.py -etc/dbus-serialbattery/battery.py -etc/dbus-serialbattery/utils.py -etc/dbus-serialbattery/lltjbd.py -etc/dbus-serialbattery/daly.py -etc/dbus-serialbattery/ant.py -etc/dbus-serialbattery/util_max17853.py -etc/dbus-serialbattery/mnb.py -etc/dbus-serialbattery/jkbms.py -etc/dbus-serialbattery/jkbms_ble.py -etc/dbus-serialbattery/jkbms_brn.py -etc/dbus-serialbattery/sinowealth.py -etc/dbus-serialbattery/renogy.py -etc/dbus-serialbattery/ecs.py -etc/dbus-serialbattery/lifepower.py diff --git a/create_build.sh b/create_build.sh deleted file mode 100644 index 55d48fe9..00000000 --- a/create_build.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -dos2unix buildfiles.lst -cat buildfiles.lst | xargs dos2unix -tar -czvf venus-data.tar.gz --mode='a+rwX' -T buildfiles.lst diff --git a/etc/dbus-serialbattery/ant.py b/etc/dbus-serialbattery/ant.py index 215a692f..07b58d11 100644 --- a/etc/dbus-serialbattery/ant.py +++ b/etc/dbus-serialbattery/ant.py @@ -5,8 +5,8 @@ class Ant(Battery): - def __init__(self, port, baud): - super(Ant, self).__init__(port, baud) + def __init__(self, port, baud, address): + super(Ant, self).__init__(port, baud, address) self.type = self.BATTERYTYPE command_general = b"\xDB\xDB\x00\x00\x00\x00" diff --git a/etc/dbus-serialbattery/battery.py b/etc/dbus-serialbattery/battery.py index 28ac2412..5abce0b1 100644 --- a/etc/dbus-serialbattery/battery.py +++ b/etc/dbus-serialbattery/battery.py @@ -1,28 +1,40 @@ # -*- coding: utf-8 -*- -from utils import * +from typing import Union, Tuple, List + +from utils import logger +import utils +import logging import math -from datetime import timedelta from time import time +from abc import ABC, abstractmethod class Protection(object): - # 2 = Alarm, 1 = Warning, 0 = Normal + """ + This class holds Warning and alarm states for different types of Checks + They are of type integer, 2 represents an Alarm, 1 a Warning, 0 if everything is fine + """ + def __init__(self): - self.voltage_high = None - self.voltage_low = None - self.voltage_cell_low = None - self.soc_low = None - self.current_over = None - self.current_under = None - self.cell_imbalance = None - self.internal_failure = None - self.temp_high_charge = None - self.temp_low_charge = None - self.temp_high_discharge = None - self.temp_low_discharge = None + self.voltage_high: int = None + self.voltage_low: int = None + self.voltage_cell_low: int = None + self.soc_low: int = None + self.current_over: int = None + self.current_under: int = None + self.cell_imbalance: int = None + self.internal_failure: int = None + self.temp_high_charge: int = None + self.temp_low_charge: int = None + self.temp_high_discharge: int = None + self.temp_low_discharge: int = None class Cell: + """ + This class holds information about a single Cell + """ + voltage = None balance = None temp = None @@ -31,8 +43,14 @@ def __init__(self, balance): self.balance = balance -class Battery(object): - def __init__(self, port, baud): +class Battery(ABC): + """ + This Class is the abstract baseclass for all batteries. For each BMS this class needs to be extended + and the abstract methods need to be implemented. The main program in dbus-serialbattery.py will then + 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" @@ -51,6 +69,7 @@ def __init__(self, port, baud): 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 @@ -59,10 +78,11 @@ def __init__(self, port, baud): self.temp1 = None self.temp2 = None self.temp_mos = None - self.cells = [] + self.cells: List[Cell] = [] self.control_charging = None self.control_voltage = None self.allow_max_voltage = True + self.control_voltage_last_set = 0 self.max_voltage_start_time = None self.control_current = None self.control_previous_total = None @@ -75,30 +95,48 @@ def __init__(self, port, baud): self.max_battery_charge_current = None self.max_battery_discharge_current = None - self.time_to_soc_update = TIME_TO_SOC_LOOP_CYCLES - - def test_connection(self): + @abstractmethod + def test_connection(self) -> bool: + """ + This abstract method needs to be implemented for each BMS. It shoudl return true if a connection + to the BMS can be established, false otherwise. + :return: the success state + """ # Each driver must override this function to test if a connection can be made - # return false when fail, true if successful + # return false when failed, true if successful return False - def get_settings(self): - # Each driver must override this function to read/set the battery settings - # It is called once after a successful connection by DbusHelper.setup_vedbus() - # Values: battery_type, version, hardware_version, min_battery_voltage, max_battery_voltage, - # MAX_BATTERY_CHARGE_CURRENT, MAX_BATTERY_DISCHARGE_CURRENT, cell_count, capacity - # return false when fail, true if successful + @abstractmethod + def get_settings(self) -> bool: + """ + Each driver must override this function to read/set the battery settings + It is called once after a successful connection by DbusHelper.setup_vedbus() + Values: battery_type, version, hardware_version, min_battery_voltage, max_battery_voltage, + MAX_BATTERY_CHARGE_CURRENT, MAX_BATTERY_DISCHARGE_CURRENT, cell_count, capacity + + :return: false when fail, true if successful + """ return False - def refresh_data(self): - # Each driver must override this function to read battery data and populate this class - # It is called each poll just before the data is published to vedbus - # return false when fail, true if successful + @abstractmethod + def refresh_data(self) -> bool: + """ + Each driver must override this function to read battery data and populate this class + It is called each poll just before the data is published to vedbus + + :return: false when fail, true if successful + """ return False - def to_temp(self, sensor, value): - # Keep the temp value between -20 and 100 to handle sensor issues or no data. - # The BMS should have already protected before those limits have been reached. + def to_temp(self, sensor: int, value: float) -> None: + """ + Keep the temp value between -20 and 100 to handle sensor issues or no data. + The BMS should have already protected before those limits have been reached. + + :param sensor: temperature sensor number + :param value: the sensor value + :return: + """ if sensor == 1: self.temp1 = min(max(value, -20), 100) if sensor == 2: @@ -106,82 +144,144 @@ def to_temp(self, sensor, value): if sensor == 'mos': self.temp_mos = min(max(value, -20), 100) - def manage_charge_voltage(self): - if LINEAR_LIMITATION_ENABLE: + def manage_charge_voltage(self) -> None: + """ + manages the charge voltage by setting self.control_voltage + :return: None + """ + if utils.LINEAR_LIMITATION_ENABLE: self.manage_charge_voltage_linear() else: self.manage_charge_voltage_step() - def manage_charge_voltage_linear(self): + def manage_charge_voltage_linear(self) -> None: + """ + manages the charge voltage using linear interpolation by setting self.control_voltage + :return: None + """ foundHighCellVoltage = False - if CVCM_ENABLE: - currentBatteryVoltage = 0 - penaltySum = 0 + voltageSum = 0 + penaltySum = 0 + tDiff = 0 + if utils.CVCM_ENABLE: + # calculate battery sum for i in range(self.cell_count): - cv = self.get_cell_voltage(i) - if cv: - currentBatteryVoltage += cv + voltage = self.get_cell_voltage(i) + if voltage: + voltageSum += voltage - if cv >= PENALTY_AT_CELL_VOLTAGE[0]: + # calculate penalty sum to prevent single cell overcharge + if voltage >= utils.PENALTY_AT_CELL_VOLTAGE[0]: + # foundHighCellVoltage: reset to False is not needed, since it is recalculated every second foundHighCellVoltage = True - penaltySum += calcLinearRelationship( - cv, PENALTY_AT_CELL_VOLTAGE, PENALTY_BATTERY_VOLTAGE + penaltySum += utils.calcLinearRelationship( + voltage, + utils.PENALTY_AT_CELL_VOLTAGE, + utils.PENALTY_BATTERY_VOLTAGE, ) - self.voltage = currentBatteryVoltage # for testing - if foundHighCellVoltage: - # Keep penalty above min battery voltage - self.control_voltage = max( - currentBatteryVoltage - penaltySum, MIN_CELL_VOLTAGE * self.cell_count - ) + voltageSum = round(voltageSum, 3) + + if self.max_voltage_start_time is None: + if ( + utils.MAX_CELL_VOLTAGE * self.cell_count <= voltageSum + and self.allow_max_voltage + ): + self.max_voltage_start_time = time() + elif ( + utils.SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT > self.soc + and not self.allow_max_voltage + ): + self.allow_max_voltage = True + else: + tDiff = time() - self.max_voltage_start_time + if utils.MAX_VOLTAGE_TIME_SEC < tDiff: + self.allow_max_voltage = False + self.max_voltage_start_time = None + + # INFO: battery will only switch to Absorption if all cells are balanced.reach MAC_CELL_VOLTAGE * cell count if they are all balanced. + if ( + foundHighCellVoltage + and self.allow_max_voltage + ): + # set CVL only once every PENALTY_RECALCULATE_EVERY seconds + control_voltage_time = int(time() / utils.PENALTY_RECALCULATE_EVERY) + if control_voltage_time != self.control_voltage_last_set: + # Keep penalty above min battery voltage + self.control_voltage = round( max( + voltageSum - penaltySum, + utils.MIN_CELL_VOLTAGE * self.cell_count, + ), 3) + self.control_voltage_last_set = control_voltage_time + + elif self.allow_max_voltage: + self.control_voltage = round( (utils.MAX_CELL_VOLTAGE * self.cell_count), 3) + else: - self.control_voltage = FLOAT_CELL_VOLTAGE * self.cell_count + self.control_voltage = round( (utils.FLOAT_CELL_VOLTAGE * self.cell_count), 3) - def manage_charge_voltage_step(self): + def manage_charge_voltage_step(self) -> None: + """ + manages the charge voltage using a step function by setting self.control_voltage + :return: None + """ voltageSum = 0 - if CVCM_ENABLE: + tDiff = 0 + if utils.CVCM_ENABLE: + + # calculate battery sum for i in range(self.cell_count): voltage = self.get_cell_voltage(i) if voltage: voltageSum += voltage - if None == self.max_voltage_start_time: + if self.max_voltage_start_time is None: + # check if max voltage is reached and start timer to keep max voltage if ( - MAX_CELL_VOLTAGE * self.cell_count <= voltageSum - and True == self.allow_max_voltage + utils.MAX_CELL_VOLTAGE * self.cell_count <= voltageSum + and self.allow_max_voltage ): + # example 2 self.max_voltage_start_time = 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 + and not self.allow_max_voltage + ): + self.allow_max_voltage = True + + # do nothing else: - if ( - SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT > self.soc - and not self.allow_max_voltage - ): - self.allow_max_voltage = True + pass + + # timer started else: tDiff = time() - self.max_voltage_start_time - if MAX_VOLTAGE_TIME_SEC < tDiff: - self.max_voltage_start_time = None + if utils.MAX_VOLTAGE_TIME_SEC < tDiff: self.allow_max_voltage = False + self.max_voltage_start_time = None + + else: + pass if self.allow_max_voltage: - # Keep penalty above min battery voltage - self.control_voltage = max( - MAX_CELL_VOLTAGE * self.cell_count, MIN_CELL_VOLTAGE * self.cell_count - ) + self.control_voltage = utils.MAX_CELL_VOLTAGE * self.cell_count else: - self.control_voltage = FLOAT_CELL_VOLTAGE * self.cell_count + self.control_voltage = utils.FLOAT_CELL_VOLTAGE * self.cell_count - def manage_charge_current(self): + def manage_charge_current(self) -> None: # Manage Charge Current Limitations charge_limits = [self.max_battery_charge_current] - if CCCM_SOC_ENABLE: + if utils.CCCM_SOC_ENABLE: charge_limits.append(self.calcMaxChargeCurrentReferringToSoc()) - if CCCM_CV_ENABLE: + if utils.CCCM_CV_ENABLE: charge_limits.append(self.calcMaxChargeCurrentReferringToCellVoltage()) - if CCCM_T_ENABLE: + if utils.CCCM_T_ENABLE: charge_limits.append(self.calcMaxChargeCurrentReferringToTemperature()) - self.control_charge_current = min(charge_limits) + self.control_charge_current = round( min(charge_limits), 3) if self.control_charge_current == 0: self.control_allow_charge = False @@ -190,145 +290,154 @@ def manage_charge_current(self): # Manage Discharge Current Limitations discharge_limits = [self.max_battery_discharge_current] - if DCCM_SOC_ENABLE: + if utils.DCCM_SOC_ENABLE: discharge_limits.append(self.calcMaxDischargeCurrentReferringToSoc()) - if DCCM_CV_ENABLE: + if utils.DCCM_CV_ENABLE: discharge_limits.append( self.calcMaxDischargeCurrentReferringToCellVoltage() ) - if DCCM_T_ENABLE: + if utils.DCCM_T_ENABLE: discharge_limits.append( self.calcMaxDischargeCurrentReferringToTemperature() ) - self.control_discharge_current = min(discharge_limits) + self.control_discharge_current = round( min(discharge_limits), 3) if self.control_discharge_current == 0: self.control_allow_discharge = False else: self.control_allow_discharge = True - def calcMaxChargeCurrentReferringToCellVoltage(self): + def calcMaxChargeCurrentReferringToCellVoltage(self) -> float: try: - if LINEAR_LIMITATION_ENABLE: - return calcLinearRelationship( + if utils.LINEAR_LIMITATION_ENABLE: + return utils.calcLinearRelationship( self.get_max_cell_voltage(), - CELL_VOLTAGES_WHILE_CHARGING, - MAX_CHARGE_CURRENT_CV, + utils.CELL_VOLTAGES_WHILE_CHARGING, + utils.MAX_CHARGE_CURRENT_CV, ) - return calcStepRelationship( + return utils.calcStepRelationship( self.get_max_cell_voltage(), - CELL_VOLTAGES_WHILE_CHARGING, - MAX_CHARGE_CURRENT_CV, + utils.CELL_VOLTAGES_WHILE_CHARGING, + utils.MAX_CHARGE_CURRENT_CV, False, ) - except: + except Exception: return self.max_battery_charge_current - def calcMaxDischargeCurrentReferringToCellVoltage(self): + def calcMaxDischargeCurrentReferringToCellVoltage(self) -> float: try: - if LINEAR_LIMITATION_ENABLE: - return calcLinearRelationship( + if utils.LINEAR_LIMITATION_ENABLE: + return utils.calcLinearRelationship( self.get_min_cell_voltage(), - CELL_VOLTAGES_WHILE_DISCHARGING, - MAX_DISCHARGE_CURRENT_CV, + utils.CELL_VOLTAGES_WHILE_DISCHARGING, + utils.MAX_DISCHARGE_CURRENT_CV, ) - return calcStepRelationship( + return utils.calcStepRelationship( self.get_min_cell_voltage(), - CELL_VOLTAGES_WHILE_DISCHARGING, - MAX_DISCHARGE_CURRENT_CV, + utils.CELL_VOLTAGES_WHILE_DISCHARGING, + utils.MAX_DISCHARGE_CURRENT_CV, True, ) - except: + except Exception: return self.max_battery_charge_current - def calcMaxChargeCurrentReferringToTemperature(self): + def calcMaxChargeCurrentReferringToTemperature(self) -> float: if self.get_max_temp() is None: return self.max_battery_charge_current temps = {0: self.get_max_temp(), 1: self.get_min_temp()} for key, currentMaxTemperature in temps.items(): - if LINEAR_LIMITATION_ENABLE: - temps[key] = calcLinearRelationship( + if utils.LINEAR_LIMITATION_ENABLE: + temps[key] = utils.calcLinearRelationship( currentMaxTemperature, - TEMPERATURE_LIMITS_WHILE_CHARGING, - MAX_CHARGE_CURRENT_T, + utils.TEMPERATURE_LIMITS_WHILE_CHARGING, + utils.MAX_CHARGE_CURRENT_T, ) else: - temps[key] = calcStepRelationship( + temps[key] = utils.calcStepRelationship( currentMaxTemperature, - TEMPERATURE_LIMITS_WHILE_CHARGING, - MAX_CHARGE_CURRENT_T, + utils.TEMPERATURE_LIMITS_WHILE_CHARGING, + utils.MAX_CHARGE_CURRENT_T, False, ) return min(temps[0], temps[1]) - def calcMaxDischargeCurrentReferringToTemperature(self): + def calcMaxDischargeCurrentReferringToTemperature(self) -> float: if self.get_max_temp() is None: return self.max_battery_discharge_current temps = {0: self.get_max_temp(), 1: self.get_min_temp()} for key, currentMaxTemperature in temps.items(): - if LINEAR_LIMITATION_ENABLE: - temps[key] = calcLinearRelationship( + if utils.LINEAR_LIMITATION_ENABLE: + temps[key] = utils.calcLinearRelationship( currentMaxTemperature, - TEMPERATURE_LIMITS_WHILE_DISCHARGING, - MAX_DISCHARGE_CURRENT_T, + utils.TEMPERATURE_LIMITS_WHILE_DISCHARGING, + utils.MAX_DISCHARGE_CURRENT_T, ) else: - temps[key] = calcStepRelationship( + temps[key] = utils.calcStepRelationship( currentMaxTemperature, - TEMPERATURE_LIMITS_WHILE_DISCHARGING, - MAX_DISCHARGE_CURRENT_T, + utils.TEMPERATURE_LIMITS_WHILE_DISCHARGING, + utils.MAX_DISCHARGE_CURRENT_T, True, ) return min(temps[0], temps[1]) - def calcMaxChargeCurrentReferringToSoc(self): + def calcMaxChargeCurrentReferringToSoc(self) -> float: try: # Create value list. Will more this to the settings object - SOC_WHILE_CHARGING = [100, CC_SOC_LIMIT1, CC_SOC_LIMIT2, CC_SOC_LIMIT3] + SOC_WHILE_CHARGING = [ + 100, + utils.CC_SOC_LIMIT1, + utils.CC_SOC_LIMIT2, + utils.CC_SOC_LIMIT3, + ] MAX_CHARGE_CURRENT_SOC = [ - CC_CURRENT_LIMIT1, - CC_CURRENT_LIMIT2, - CC_CURRENT_LIMIT3, - MAX_BATTERY_CHARGE_CURRENT, + utils.CC_CURRENT_LIMIT1, + utils.CC_CURRENT_LIMIT2, + utils.CC_CURRENT_LIMIT3, + utils.MAX_BATTERY_CHARGE_CURRENT, ] - if LINEAR_LIMITATION_ENABLE: - return calcLinearRelationship( + if utils.LINEAR_LIMITATION_ENABLE: + return utils.calcLinearRelationship( self.soc, SOC_WHILE_CHARGING, MAX_CHARGE_CURRENT_SOC ) - return calcStepRelationship( + return utils.calcStepRelationship( self.soc, SOC_WHILE_CHARGING, MAX_CHARGE_CURRENT_SOC, True ) - except: + except Exception: return self.max_battery_charge_current - def calcMaxDischargeCurrentReferringToSoc(self): + def calcMaxDischargeCurrentReferringToSoc(self) -> float: try: # Create value list. Will more this to the settings object - SOC_WHILE_DISCHARGING = [DC_SOC_LIMIT3, DC_SOC_LIMIT2, DC_SOC_LIMIT1] + SOC_WHILE_DISCHARGING = [ + utils.DC_SOC_LIMIT3, + utils.DC_SOC_LIMIT2, + utils.DC_SOC_LIMIT1, + ] MAX_DISCHARGE_CURRENT_SOC = [ - MAX_BATTERY_DISCHARGE_CURRENT, - DC_CURRENT_LIMIT3, - DC_CURRENT_LIMIT2, - DC_CURRENT_LIMIT1, + utils.MAX_BATTERY_DISCHARGE_CURRENT, + utils.DC_CURRENT_LIMIT3, + utils.DC_CURRENT_LIMIT2, + utils.DC_CURRENT_LIMIT1, ] - if LINEAR_LIMITATION_ENABLE: - return calcLinearRelationship( + if utils.LINEAR_LIMITATION_ENABLE: + return utils.calcLinearRelationship( self.soc, SOC_WHILE_DISCHARGING, MAX_DISCHARGE_CURRENT_SOC ) - return calcStepRelationship( + return utils.calcStepRelationship( self.soc, SOC_WHILE_DISCHARGING, MAX_DISCHARGE_CURRENT_SOC, True ) - except: + except Exception: return self.max_battery_charge_current - def get_min_cell(self): + def get_min_cell(self) -> int: min_voltage = 9999 min_cell = None if len(self.cells) == 0 and hasattr(self, "cell_min_no"): @@ -343,7 +452,7 @@ def get_min_cell(self): min_cell = c return min_cell - def get_max_cell(self): + def get_max_cell(self) -> int: max_voltage = 0 max_cell = None if len(self.cells) == 0 and hasattr(self, "cell_max_no"): @@ -358,56 +467,83 @@ def get_max_cell(self): max_cell = c return max_cell - def get_min_cell_desc(self): + def get_min_cell_desc(self) -> Union[str, None]: cell_no = self.get_min_cell() return cell_no if cell_no is None else "C" + str(cell_no + 1) - def get_max_cell_desc(self): + 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): + def get_cell_voltage(self, idx) -> 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): + def get_cell_balancing(self, idx) -> 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: return 1 return 0 - def get_capacity_remain(self): + 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 return None - def get_timetosoc(self, socnum, crntPrctPerSec): + def get_timeToSoc(self, socnum, crntPrctPerSec, onlyNumber = False) -> str: if self.current > 0: diffSoc = socnum - self.soc else: diffSoc = self.soc - socnum ttgStr = None - if self.soc != socnum and (diffSoc > 0 or TIME_TO_SOC_INC_FROM is True): + if self.soc != socnum and (diffSoc > 0 or utils.TIME_TO_SOC_INC_FROM is True): secondstogo = int(diffSoc / crntPrctPerSec) ttgStr = "" - if TIME_TO_SOC_VALUE_TYPE & 1: + if onlyNumber or utils.TIME_TO_SOC_VALUE_TYPE & 1: ttgStr += str(secondstogo) - if TIME_TO_SOC_VALUE_TYPE & 2: + if not onlyNumber and utils.TIME_TO_SOC_VALUE_TYPE & 2: ttgStr += " [" - if TIME_TO_SOC_VALUE_TYPE & 2: - ttgStr += str(timedelta(seconds=secondstogo)) - if TIME_TO_SOC_VALUE_TYPE & 1: + if not onlyNumber and utils.TIME_TO_SOC_VALUE_TYPE & 2: + ttgStr += self.get_secondsToString(secondstogo) + + if utils.TIME_TO_SOC_VALUE_TYPE & 1: ttgStr += "]" return ttgStr - def get_min_cell_voltage(self): + def get_secondsToString(self, timespan, precision = 3) -> str: + """ + Transforms seconds to a string in the format: 1d 1h 1m 1s (Victron Style) + :param precision: + 0 = 1d + 1 = 1d 1h + 2 = 1d 1h 1m + 3 = 1d 1h 1m 1s + + This was added, since timedelta() returns strange values, if time is negative + e.g.: seconds: -70245 --> timedelta output: -1 day, 4:29:15 --> calculation: -1 day + 4:29:15 --> real value -19:30:45 + """ + tmp = '' if timespan >= 0 else '-' + timespan = abs(timespan) + + m, s = divmod(timespan, 60) + h, m = divmod(m, 60) + d, h = divmod(h, 24) + + tmp += ( ( str(d) + 'd ' ) if d > 0 else '' ) + tmp += ( ( str(h) + 'h ' ) if precision >= 1 and h > 0 else '' ) + tmp += ( ( str(m) + 'm ' ) if precision >= 2 and m > 0 else '' ) + tmp += ( ( str(s) + 's ' ) if precision == 3 and s > 0 else '' ) + + return tmp.rstrip() + + def get_min_cell_voltage(self) -> Union[float, None]: min_voltage = None if hasattr(self, "cell_min_voltage"): min_voltage = self.cell_min_voltage @@ -421,7 +557,7 @@ def get_min_cell_voltage(self): pass return min_voltage - def get_max_cell_voltage(self): + def get_max_cell_voltage(self) -> Union[float, None]: max_voltage = None if hasattr(self, "cell_max_voltage"): max_voltage = self.cell_max_voltage @@ -435,9 +571,15 @@ def get_max_cell_voltage(self): pass return max_voltage - def get_midvoltage(self): + def get_midvoltage(self) -> Tuple[Union[float, None], Union[float, None]]: + """ + This method returns the Voltage "in the middle of the battery" + as well as a deviation of an ideally balanced battery. It does so by calculating the sum of the first half + of the cells and adding 1/2 of the "middle cell" voltage (if it exists) + :return: a tuple of the voltage in the middle, as well as a percentage deviation (total_voltage / 2) + """ if ( - not MIDPOINT_ENABLE + not utils.MIDPOINT_ENABLE or self.cell_count is None or self.cell_count == 0 or self.cell_count < 4 @@ -446,30 +588,28 @@ def get_midvoltage(self): return None, None halfcount = int(math.floor(self.cell_count / 2)) + uneven_cells_offset = self.cell_count % 2 half1voltage = 0 half2voltage = 0 try: half1voltage = sum( - c.voltage for c in self.cells[:halfcount] if c.voltage is not None + cell.voltage + for cell in self.cells[:halfcount] + if cell.voltage is not None ) half2voltage = sum( - c.voltage - for c in self.cells[halfcount : halfcount * 2] - if c.voltage is not None + cell.voltage + for cell in self.cells[halfcount + uneven_cells_offset :] + if cell.voltage is not None ) except ValueError: pass try: - # handle uneven cells by giving half the voltage of the last cell to half1 and half2 - extra = ( - 0 - if (2 * halfcount == self.cell_count) - else self.cells[self.cell_count - 1].voltage / 2 - ) + extra = 0 if self.cell_count % 2 == 0 else self.cells[halfcount].voltage / 2 # get the midpoint of the battery - midpoint = (half1voltage + half2voltage) / 2 + extra + midpoint = half1voltage + extra return ( midpoint, (half2voltage - half1voltage) / (half2voltage + half1voltage) * 100, @@ -477,25 +617,15 @@ def get_midvoltage(self): except ValueError: return None, None - def get_balancing(self): + def get_balancing(self) -> int: for c in range(min(len(self.cells), self.cell_count)): if self.cells[c].balance is not None and self.cells[c].balance: return 1 return 0 - def get_temp(self): - if self.temp1 is not None and self.temp2 is not None: - return round((float(self.temp1) + float(self.temp2)) / 2, 2) - if self.temp1 is not None and self.temp2 is None: - return round(float(self.temp1), 2) - if self.temp1 is None and self.temp2 is not None: - return round(float(self.temp2), 2) - else: - return None - - def get_min_temp(self): + def extract_from_temp_values(self, extractor) -> Union[float, None]: if self.temp1 is not None and self.temp2 is not None: - return min(self.temp1, self.temp2) + return extractor(self.temp1, self.temp2) if self.temp1 is not None and self.temp2 is None: return self.temp1 if self.temp1 is None and self.temp2 is not None: @@ -503,23 +633,28 @@ def get_min_temp(self): else: return None - def get_max_temp(self): - if self.temp1 is not None and self.temp2 is not None: - return max(self.temp1, self.temp2) - if self.temp1 is not None and self.temp2 is None: - return self.temp1 - if self.temp1 is None and self.temp2 is not None: - return self.temp2 - else: - return None + def get_temp(self) -> Union[float, None]: + return self.extract_from_temp_values( + extractor=lambda temp1, temp2: round((float(temp1) + float(temp2)) / 2, 2) + ) - def get_mos_temp(self): + def get_min_temp(self) -> Union[float, None]: + return self.extract_from_temp_values( + extractor=lambda temp1, temp2: min(temp1, temp2) + ) + + def get_max_temp(self) -> Union[float, None]: + return self.extract_from_temp_values( + extractor=lambda temp1, temp2: max(temp1, temp2) + ) + + def get_mos_temp(self) -> Union[float, None]: if self.temp_mos is not None: return self.temp_mos else: return None - def log_cell_data(self): + def log_cell_data(self) -> bool: if logger.getEffectiveLevel() > logging.INFO and len(self.cells) == 0: return False @@ -531,20 +666,19 @@ def log_cell_data(self): logger.debug("Cells:" + cell_res) return True - def log_settings(self): + def log_settings(self) -> None: - logger.info(f"Battery {self.type} connected to dbus from {self.port}") - logger.info(f"=== Settings ===") cell_counter = len(self.cells) - logger.info( - f"> Connection voltage {self.voltage}V | current {self.current}A | SOC {self.soc}%" - ) - logger.info(f"> Cell count {self.cell_count} | cells populated {cell_counter}") - logger.info(f"> CCCM SOC {CCCM_SOC_ENABLE} | DCCM SOC {DCCM_SOC_ENABLE}") - logger.info(f"> CCCM CV {CCCM_CV_ENABLE} | DCCM CV {DCCM_CV_ENABLE}") - logger.info(f"> CCCM T {CCCM_T_ENABLE} | DCCM T {DCCM_T_ENABLE}") - logger.info( - f"> MIN_CELL_VOLTAGE {MIN_CELL_VOLTAGE}V | MAX_CELL_VOLTAGE {MAX_CELL_VOLTAGE}V" - ) + 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}%") + 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"> MAX BATTERY CHARGE CURRENT: {utils.MAX_BATTERY_CHARGE_CURRENT}V | MAX BATTERY DISCHARGE CURRENT: {utils.MAX_BATTERY_DISCHARGE_CURRENT}V") + 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}") + logger.info(f"> CCCM T: {str(utils.CCCM_T_ENABLE).ljust(5)} | DCCM T: {utils.DCCM_T_ENABLE}") + logger.info(f"> CCCM SOC: {str(utils.CCCM_SOC_ENABLE).ljust(5)} | DCCM SOC: {utils.DCCM_SOC_ENABLE}") return diff --git a/etc/dbus-serialbattery/battery_template.py b/etc/dbus-serialbattery/battery_template.py index b61546d6..e921efc0 100644 --- a/etc/dbus-serialbattery/battery_template.py +++ b/etc/dbus-serialbattery/battery_template.py @@ -5,8 +5,8 @@ class BatteryTemplate(Battery): - def __init__(self, port, baud): - super(BatteryTemplate, self).__init__(port, baud) + def __init__(self, port, baud, address): + super(BatteryTemplate, self).__init__(port, baud, address) self.type = self.BATTERYTYPE BATTERYTYPE = "Template" diff --git a/etc/dbus-serialbattery/config.default.ini b/etc/dbus-serialbattery/config.default.ini new file mode 100644 index 00000000..7bc2d501 --- /dev/null +++ b/etc/dbus-serialbattery/config.default.ini @@ -0,0 +1,186 @@ +[DEFAULT] + +; Choose the mode for voltage / current limitations (True / False) +; False is a step mode. This is the default with limitations on hard boundary steps +; True is a linear mode. For CCL and DCL the values between the steps are calculated for smoother values (by WaldemarFech) +; For CVL the penalties are only applied, if the cell voltage reaches the penalty voltage +LINEAR_LIMITATION_ENABLE = False + +; Battery Current limits +MAX_BATTERY_CHARGE_CURRENT = 70.0 +MAX_BATTERY_DISCHARGE_CURRENT = 90.0 + + +; --------- Charge Voltage limitation (affecting CVL) --------- +; Description: Limit max charging voltage (MAX_CELL_VOLTAGE * cell count) and switch from max voltage to float voltage (FLOAT_CELL_VOLTAGE * cell count) +; after max voltage is reached for MAX_VOLTAGE_TIME_SEC. It switches back to max voltage after SoC is below SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT +; If LINEAR_LIMITATION_ENABLE is set to True then penalty voltages are applied +; Example: 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 90% +; OR +; The battery reached max voltage of 55.2V and the max cell difference is 0.01V, then switch to float voltage of 53.6V to don't stress the batteries. +; Allow max voltage of 55.2V again if max cell difference is above 0.02V +; Charge voltage control management enable (True/False). +CVCM_ENABLE = False + +; -- 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 +MIN_CELL_VOLTAGE = 2.90 +; Max voltage can seen as absorption voltage +MAX_CELL_VOLTAGE = 3.45 +FLOAT_CELL_VOLTAGE = 3.35 + +; -- Penalty Voltages +; NOTE: works only when LINEAR_LIMITATION_ENABLE = True +; More details can be found here: https://github.com/Louisvdw/dbus-serialbattery/issues/297#issuecomment-1327142635 +; If the cell voltage reaches 3.48V, then reduce actual battery-voltage by 0.01V +; If the cell voltage goes over 3.6V, then the maximum penalty will not be exceeded +; There will be a sum of all penalties for each cell, which exceeds the limits +; NOTE: The first value of PENALTY_AT_CELL_VOLTAGE has to be at least MAX_CELL_VOLTAGE + the first value of PENALTY_BATTERY_VOLTAGE, +; else the FLOAT_CELL_VOLTAGE is never set. Additionally the battery voltage has to reach max voltage and all cells has to be below penalty voltage to switch to float voltage. +PENALTY_AT_CELL_VOLTAGE = 3.48, 3.55, 3.6 +; this voltage will be subtracted +PENALTY_BATTERY_VOLTAGE = 0.01, 1.0, 2.0 +; Specify in seconds how often the penalty should be recalculated +PENALTY_RECALCULATE_EVERY = 60 + +; -- CVL Reset based on SoC option +; Reset max voltage after +MAX_VOLTAGE_TIME_SEC = 900 +; Specify SoC where CVL limit is reset to max voltage +SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT = 90 + + +; --------- 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 + +; Charge current control management referring to cell-voltage enable (True/False). +CCCM_CV_ENABLE = True +; Discharge current control management referring to cell-voltage enable (True/False). +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_DISCHARGING = 2.70, 2.80, 2.90, 3.10 +MAX_DISCHARGE_CURRENT_CV_FRACTION = 0, 0.1, 0.5, 1 + + +; --------- 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. +; Charge current control management referring to temperature enable (True/False). +CCCM_T_ENABLE = True +; Charge current control management referring to temperature enable (True/False). +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 + +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 + + +; --------- 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. +; 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 + +; Discharge current limits +DC_CURRENT_LIMIT1_FRACTION = 0.1 +DC_CURRENT_LIMIT2_FRACTION = 0.3 +DC_CURRENT_LIMIT3_FRACTION = 0.5 + + +; --------- 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 +; 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 +; Leave empty to disable +TIME_TO_SOC_POINTS = +; Specify TimeToSoc value type [Valid values 1, 2, 3] +; 1 Seconds +; 2 Time string d h m s +; 3 Both seconds and time string " [d h m s]" +TIME_TO_SOC_VALUE_TYPE = 1 +; Specify in seconds how often the TimeToSoc should be recalculated +; Minimum are 5 seconds to prevent CPU overload +TIME_TO_SOC_RECALCULATE_EVERY = 60 +; Include TimeToSoC points when moving away from the SoC point [Valid values True, False] +; These will be as negative time. Disabling this improves performance slightly +TIME_TO_SOC_INC_FROM = False + + +; --------- Additional settings --------- +; Specify only one BMS type to load else leave empty to try to load all availabe +; LltJbd, Ant, Daly, Daly, Jkbms, Lifepower, Renogy, Renogy, Ecs +BMS_TYPE = + +; Publish the config settings to the dbus path "/Info/Config/" +PUBLISH_CONFIG_VALUES = 1 + +; 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) +; 1 Format: /Voltages/Cell (also available for display on Remote Console) +; 2 Format: /Cell/#/Volts +; 3 Both formats 1 and 2 +BATTERY_CELL_DATA_FORMAT = 1 + +; Simulate Midpoint graph (True/False). +MIDPOINT_ENABLE = False + + +; --------- BMS specific settings --------- + +; -- 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 +SOC_LOW_WARNING = 20 +SOC_LOW_ALARM = 10 + +; -- Daly settings +; Battery capacity (amps) if the BMS does not support reading it +BATTERY_CAPACITY = 50 +; Invert Battery Current. Default non-inverted. Set to -1 to invert +INVERT_CURRENT_MEASUREMENT = 1 + +; -- ESC GreenMeter and Lipro device settings +GREENMETER_ADDRESS = 1 +LIPRO_START_ADDRESS = 2 +LIPRO_END_ADDRESS = 4 +LIPRO_CELL_COUNT = 15 diff --git a/etc/dbus-serialbattery/daly.py b/etc/dbus-serialbattery/daly.py index 10281cbd..ae96c723 100644 --- a/etc/dbus-serialbattery/daly.py +++ b/etc/dbus-serialbattery/daly.py @@ -6,7 +6,7 @@ class Daly(Battery): def __init__(self, port, baud, address): - super(Daly, self).__init__(port, baud) + super(Daly, self).__init__(port, baud, address) self.charger_connected = None self.load_connected = None self.command_address = address diff --git a/etc/dbus-serialbattery/dbus-serialbattery.py b/etc/dbus-serialbattery/dbus-serialbattery.py index 1f07858e..a03293e6 100644 --- a/etc/dbus-serialbattery/dbus-serialbattery.py +++ b/etc/dbus-serialbattery/dbus-serialbattery.py @@ -1,9 +1,10 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +from typing import Union + from time import sleep from dbus.mainloop.glib import DBusGMainLoop from threading import Thread -import dbus import sys if sys.version_info.major == 2: @@ -15,20 +16,37 @@ # from ve_utils import exit_on_error from dbushelper import DbusHelper -from utils import DRIVER_VERSION, DRIVER_SUBVERSION, logger, battery_types -import logging +from utils import logger +import utils +from battery import Battery from lltjbd import LltJbd from daly import Daly from ant import Ant from jkbms import Jkbms from jkbms_ble import Jkbms_Ble -from sinowealth import Sinowealth + +# from sinowealth import Sinowealth from renogy import Renogy from ecs import Ecs from lifepower import Lifepower -# from mnb import MNB - +supported_bms_types = [ + {"bms": LltJbd, "baud": 9600}, + {"bms": Ant, "baud": 19200}, + {"bms": Daly, "baud": 9600, "address": b"\x40"}, + {"bms": Daly, "baud": 9600, "address": b"\x80"}, + {"bms": Jkbms, "baud": 115200}, + # {"bms" : Sinowealth}, + {"bms": Lifepower, "baud": 9600}, + {"bms": Renogy, "baud": 9600, "address": b"\x30"}, + {"bms": Renogy, "baud": 9600, "address": b"\xF7"}, + {"bms": Ecs, "baud": 19200}, +] +expected_bms_types = [ + battery_type + for battery_type in supported_bms_types + if battery_type["bms"].__name__ == utils.BMS_TYPE or utils.BMS_TYPE == "" +] logger.info("Starting dbus-serialbattery") @@ -42,34 +60,30 @@ def poll_battery(loop): poller.start() return True - def get_battery_type(_port): + def get_battery(_port) -> Union[Battery, None]: # all the different batteries the driver support and need to test for # try to establish communications with the battery 3 times, else exit count = 3 while count > 0: # create a new battery object that can read the battery and run connection test - for test in battery_types: - logger.info("Testing " + test["bms"]) - class_ = eval(test["bms"]) - if "baud" in test.keys(): - baud = test["baud"] - else: - baud = 9600 - if "address" in test.keys(): - testbms = class_(_port, baud, test["address"]) - else: - testbms = class_(_port, baud) - if testbms.test_connection() is True: + for test in expected_bms_types: + logger.info("Testing " + test["bms"].__name__) + batteryClass = test["bms"] + baud = test["baud"] + battery: Battery = batteryClass( + port=_port, baud=baud, address=test.get("address") + ) + if battery.test_connection(): logger.info( - "Connection established to " + testbms.__class__.__name__ + "Connection established to " + battery.__class__.__name__ ) - return testbms + return battery count -= 1 sleep(0.5) return None - def get_port(): + def get_port() -> str: # Get the port we need to use from the argument if len(sys.argv) > 1: return sys.argv[1] @@ -78,7 +92,9 @@ def get_port(): logger.info("No Port needed") return "/dev/tty/USB9" - logger.info("dbus-serialbattery v" + str(DRIVER_VERSION) + DRIVER_SUBVERSION) + logger.info( + "dbus-serialbattery v" + str(utils.DRIVER_VERSION) + utils.DRIVER_SUBVERSION + ) port = get_port() battery = None diff --git a/etc/dbus-serialbattery/dbushelper.py b/etc/dbus-serialbattery/dbushelper.py index 35b0512e..6cfc3260 100644 --- a/etc/dbus-serialbattery/dbushelper.py +++ b/etc/dbus-serialbattery/dbushelper.py @@ -4,6 +4,7 @@ import platform import dbus import traceback +from time import time # Victron packages sys.path.insert( @@ -296,9 +297,21 @@ def setup_vedbus(self): gettextcallback=lambda p, v: "{:0.3f}V".format(v), ) - # Create TimeToSoC items - for num in TIME_TO_SOC_POINTS: - self._dbusservice.add_path("/TimeToSoC/" + str(num), None, writeable=True) + # Create TimeToSoC items only if enabled + if ( + self.battery.capacity is not None + and len(TIME_TO_SOC_POINTS) > 0 + ): + # Create TimeToGo item + self._dbusservice.add_path("/TimeToGo", None, writeable=True) + + # Create TimeToSoc items + for num in TIME_TO_SOC_POINTS: + self._dbusservice.add_path("/TimeToSoC/" + str(num), None, writeable=True) + + logger.info(f"publish config values = {PUBLISH_CONFIG_VALUES}") + if PUBLISH_CONFIG_VALUES == 1: + publish_config_variables(self._dbusservice) return True @@ -325,7 +338,7 @@ def publish_battery(self, loop): # This is to mannage CVCL self.battery.manage_charge_voltage() - # publish all the data fro the battery object to dbus + # publish all the data from the battery object to dbus self.publish_dbus() except: @@ -345,7 +358,7 @@ def publish_dbus(self): self._dbusservice["/Dc/0/Temperature"] = self.battery.get_temp() self._dbusservice["/Capacity"] = self.battery.get_capacity_remain() self._dbusservice["/ConsumedAmphours"] = ( - 0 + None if self.battery.capacity is None or self.battery.get_capacity_remain() is None else self.battery.capacity - self.battery.get_capacity_remain() @@ -363,7 +376,9 @@ def publish_dbus(self): 1 if self.battery.charge_fet and self.battery.control_allow_charge else 0 ) self._dbusservice["/Io/AllowToDischarge"] = ( - 1 if self.battery.discharge_fet else 0 + 1 + if self.battery.discharge_fet and self.battery.control_allow_discharge + else 0 ) self._dbusservice["/Io/AllowToBalance"] = ( 1 if self.battery.balance_fet else 0 @@ -386,12 +401,8 @@ def publish_dbus(self): self._dbusservice["/System/MOSTemperature"] = self.battery.get_mos_temp() # 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 control self._dbusservice["/Info/MaxChargeVoltage"] = self.battery.control_voltage @@ -399,45 +410,23 @@ def publish_dbus(self): # 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 self._dbusservice["/Alarms/HighVoltage"] = self.battery.protection.voltage_high 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 # cell voltages if BATTERY_CELL_DATA_FORMAT > 0: @@ -470,23 +459,42 @@ def publish_dbus(self): try: if ( self.battery.capacity is not None - and len(TIME_TO_SOC_POINTS) > 0 - and self.battery.time_to_soc_update == 0 + and + len(TIME_TO_SOC_POINTS) > 0 + and + ( + ( + # update only once in same second + int(time()) != self.battery.time_to_soc_update + and + # update only every x seconds + int(time()) % TIME_TO_SOC_RECALCULATE_EVERY == 0 + ) + or + # update on first run + self.battery.time_to_soc_update == 0 + ) ): - self.battery.time_to_soc_update = TIME_TO_SOC_LOOP_CYCLES + self.battery.time_to_soc_update = int(time()) crntPrctPerSec = ( abs(self.battery.current / (self.battery.capacity / 100)) / 3600 ) + # Update TimeToGo item, has to be a positive int since it's used from dbus-systemcalc-py + self._dbusservice["/TimeToGo"] = ( + abs ( int ( self.battery.get_timeToSoc(SOC_LOW_WARNING, crntPrctPerSec, True) ) ) + if self.battery.current + else None + ) + + # Update TimeToSoc items for num in TIME_TO_SOC_POINTS: self._dbusservice["/TimeToSoC/" + str(num)] = ( - self.battery.get_timetosoc(num, crntPrctPerSec) + self.battery.get_timeToSoc(num, crntPrctPerSec) if self.battery.current else None ) - else: - self.battery.time_to_soc_update -= 1 except: pass diff --git a/etc/dbus-serialbattery/disabledriver.sh b/etc/dbus-serialbattery/disabledriver.sh index 1b141d39..28afde26 100644 --- a/etc/dbus-serialbattery/disabledriver.sh +++ b/etc/dbus-serialbattery/disabledriver.sh @@ -1,2 +1,18 @@ -#!/bin/sh -rm -f /data/conf/serial-starter.d \ No newline at end of file +#!/bin/bash +set -x + +DRIVERNAME=dbus-serialbattery + +# handle read only mounts +sh /opt/victronenergy/swupdate-scripts/remount-rw.sh + +# remove files +rm -f /data/conf/serial-starter.d +rm -rf /service/dbus-blebattery-* + +# kill if running +pkill -f "python .*/$DRIVERNAME.py" + +# remove install-script from rc.local +sed -i "/sh \/data\/etc\/$DRIVERNAME\/reinstalllocal.sh/d" /data/rc.local +sed -i "/sh \/data\/etc\/$DRIVERNAME\/installble.sh/d" /data/rc.local diff --git a/etc/dbus-serialbattery/ecs.py b/etc/dbus-serialbattery/ecs.py index 598cc58c..d8bc7295 100644 --- a/etc/dbus-serialbattery/ecs.py +++ b/etc/dbus-serialbattery/ecs.py @@ -7,8 +7,8 @@ class Ecs(Battery): - def __init__(self, port, baud): - super(Ecs, self).__init__(port, baud) + def __init__(self, port, baud, address): + super(Ecs, self).__init__(port, baud, address) self.type = self.BATTERYTYPE BATTERYTYPE = "ECS_LiPro" diff --git a/etc/dbus-serialbattery/installble.sh b/etc/dbus-serialbattery/installble.sh index 442d86bd..6395c64a 100755 --- a/etc/dbus-serialbattery/installble.sh +++ b/etc/dbus-serialbattery/installble.sh @@ -1,4 +1,5 @@ -#!/bin/sh +#!/bin/bash +set -x ## DO NOT TOUCH THIS ## install_service() { @@ -6,6 +7,7 @@ install_service() { echo "#!/bin/sh" > /service/dbus-blebattery-$1/log/run echo "exec multilog t s25000 n4 /var/log/dbus-blebattery-$1" >> /service/dbus-blebattery-$1/log/run chmod 755 /service/dbus-blebattery-$1/log/run + echo "#!/bin/sh" > /service/dbus-blebattery-$1/run echo "exec 2>&1" >> /service/dbus-blebattery-$1/run echo "bluetoothctl disconnect $3" >> /service/dbus-blebattery-$1/run diff --git a/etc/dbus-serialbattery/installlocal.sh b/etc/dbus-serialbattery/installlocal.sh index 1603fc59..cf8e25be 100644 --- a/etc/dbus-serialbattery/installlocal.sh +++ b/etc/dbus-serialbattery/installlocal.sh @@ -1,4 +1,5 @@ #!/bin/sh +set -x # install required packages opkg update diff --git a/etc/dbus-serialbattery/installqml.sh b/etc/dbus-serialbattery/installqml.sh index 6b472145..ed8ce289 100644 --- a/etc/dbus-serialbattery/installqml.sh +++ b/etc/dbus-serialbattery/installqml.sh @@ -30,42 +30,47 @@ function versionStringToNumber () fi } +# backup old PageBattery.qml once. New firmware upgrade will remove the backup +if [ ! -f /opt/victronenergy/gui/qml/PageBattery.qml.backup ]; then + cp /opt/victronenergy/gui/qml/PageBattery.qml /opt/victronenergy/gui/qml/PageBattery.qml.backup +fi +# backup old PageLynxIonIo.qml once. New firmware upgrade will remove the backup +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 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/ + + # get current Venus OS version versionStringToNumber $(head -n 1 /opt/victronenergy/version) ((venusVersionNumber = $versionNumber)) -# revert to VisualItemModel, if before v3.00~14 (v3.00~14 uses VisibleItemModel) +# revert to VisualItemModel, if Venus OS older than v3.00~14 (v3.00~14 uses VisibleItemModel) versionStringToNumber "v3.00~14" -qmlDir="/data/etc/dbus-serialbattery/qml" +# change in Victron directory, else the files are "broken" if upgrading from v2 to v3 +qmlDir="/opt/victronenergy/gui/qml" if (( $venusVersionNumber < $versionNumber )); then - echo "Venus OS $(head -n 1 /opt/victronenergy/version) is olter than v3.00~14. Replacing VisibleItemModel with VisualItemModel" + echo -n "Venus OS $(head -n 1 /opt/victronenergy/version) is older than v3.00~14. Replacing VisibleItemModel with VisualItemModel... " fileList="$qmlDir/PageBattery.qml" fileList+=" $qmlDir/PageBatteryCellVoltages.qml" fileList+=" $qmlDir/PageBatterySetup.qml" fileList+=" $qmlDir/PageLynxIonIo.qml" for file in $fileList ; do sed -i -e 's/VisibleItemModel/VisualItemModel/' "$file" - done + done + echo "done." fi -# backup old PageBattery.qml once. New firmware upgrade will remove the backup -if [ ! -f /opt/victronenergy/gui/qml/PageBattery.qml.backup ]; then - cp /opt/victronenergy/gui/qml/PageBattery.qml /opt/victronenergy/gui/qml/PageBattery.qml.backup -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 PageLynxIonIo.qml -cp /data/etc/dbus-serialbattery/qml/PageLynxIonIo.qml /opt/victronenergy/gui/qml/ -# copy new PageBatteryCellVoltages -cp /data/etc/dbus-serialbattery/qml/PageBatteryCellVoltages.qml /opt/victronenergy/gui/qml/ -cp /data/etc/dbus-serialbattery/qml/PageBatterySetup.qml /opt/victronenergy/gui/qml/ # stop gui svc -d /service/gui # sleep 1 sec diff --git a/etc/dbus-serialbattery/installrelease.sh b/etc/dbus-serialbattery/installrelease.sh index 530ef525..2904bbde 100644 --- a/etc/dbus-serialbattery/installrelease.sh +++ b/etc/dbus-serialbattery/installrelease.sh @@ -1,4 +1,5 @@ -#!/bin/sh -curl -s https://api.github.com/repos/Louisvdw/dbus-serialbattery/releases/latest | grep "browser_download_url.*gz" | cut -d : -f 2,3 | tr -d \" | wget -O venus-data.tar.gz -qi - +#!/bin/bash +set -x +curl -s https://api.github.com/repos/Louisvdw/dbus-serialbattery/releases/latest | grep "browser_download_url.*gz" | cut -d : -f 2,3 | tr -d \" | wget -O venus-data.tar.gz -qi - tar -zxf ./venus-data.tar.gz -C /data sh /data/etc/dbus-serialbattery/reinstalllocal.sh diff --git a/etc/dbus-serialbattery/jkbms.py b/etc/dbus-serialbattery/jkbms.py index 07baaf8e..655c29af 100644 --- a/etc/dbus-serialbattery/jkbms.py +++ b/etc/dbus-serialbattery/jkbms.py @@ -6,8 +6,8 @@ class Jkbms(Battery): - def __init__(self, port, baud): - super(Jkbms, self).__init__(port, baud) + def __init__(self, port, baud, address): + super(Jkbms, self).__init__(port, baud, address) self.type = self.BATTERYTYPE BATTERYTYPE = "Jkbms" @@ -21,14 +21,11 @@ def test_connection(self): # call a function that will connect to the battery, send a command and retrieve the result. # The result or call should be unique to this BMS. Battery name or version, etc. # Return True if success, False for failure - result = False try: - result = self.read_status_data() + return self.read_status_data() except Exception as err: logger.error(f"Unexpected {err=}, {type(err)=}") - pass - - return result + return False def get_settings(self): # After successful connection get_settings will be call to set up the battery. @@ -134,6 +131,10 @@ def read_status_data(self): self.to_fet_bits( unpack_from(">H", self.get_data(status_data, b"\x8C", offset, 2))[0] ) + offset = cellbyte_count + 84 + self.to_balance_bits( + unpack_from(">B", self.get_data(status_data, b"\x9D", offset, 1))[0] + ) offset = cellbyte_count + 155 self.production = unpack_from( @@ -144,13 +145,53 @@ def read_status_data(self): ">15s", self.get_data(status_data, b"\xB7", offset, 15) )[0].decode() + # show wich cells are balancing + if self.get_min_cell() is not None and self.get_max_cell() is not None: + for c in range(self.cell_count): + if self.balancing and ( self.get_min_cell() == c or self.get_max_cell() == c ): + self.cells[c].balance = True + else: + self.cells[c].balance = False + # logger.info(self.hardware_version) return True def to_fet_bits(self, byte_data): - tmp = bin(byte_data)[2:].rjust(2, utils.zero_char) - self.charge_fet = is_bit_set(tmp[1]) - self.discharge_fet = is_bit_set(tmp[0]) + tmp = bin(byte_data)[2:].rjust(3, utils.zero_char) + self.charge_fet = is_bit_set(tmp[2]) + self.discharge_fet = is_bit_set(tmp[1]) + self.balancing = is_bit_set(tmp[0]) + + def to_balance_bits(self, byte_data): + tmp = bin(byte_data)[2:] + self.balance_fet = is_bit_set(tmp) + + def get_balancing(self): + return 1 if self.balancing else 0 + + def get_min_cell(self): + min_voltage = 9999 + min_cell = None + for c in range(min(len(self.cells), self.cell_count)): + if ( + self.cells[c].voltage is not None + and min_voltage > self.cells[c].voltage + ): + min_voltage = self.cells[c].voltage + min_cell = c + return min_cell + + def get_max_cell(self): + max_voltage = 0 + max_cell = None + for c in range(min(len(self.cells), self.cell_count)): + if ( + self.cells[c].voltage is not None + and max_voltage < self.cells[c].voltage + ): + max_voltage = self.cells[c].voltage + max_cell = c + return max_cell def to_protection_bits(self, byte_data): pos = 13 @@ -182,8 +223,12 @@ def to_protection_bits(self, byte_data): 1 if is_bit_set(tmp[pos - 4]) or is_bit_set(tmp[pos - 8]) else 0 ) - def read_serial_data_jkbms(self, command): - # use the read_serial_data() function to read the data and then do BMS spesific checks (crc, start bytes, etc) + def read_serial_data_jkbms(self, command: str) -> bool: + """ + use the read_serial_data() function to read the data and then do BMS specific checks (crc, start bytes, etc) + :param command: the command to be sent to the bms + :return: True if everything is fine, else False + """ data = read_serial_data( command, self.port, diff --git a/etc/dbus-serialbattery/jkbms_ble.py b/etc/dbus-serialbattery/jkbms_ble.py index b94945d8..369ef848 100644 --- a/etc/dbus-serialbattery/jkbms_ble.py +++ b/etc/dbus-serialbattery/jkbms_ble.py @@ -13,7 +13,7 @@ class Jkbms_Ble(Battery): resetting = False def __init__(self, port, baud, address): - super(Jkbms_Ble, self).__init__(address.replace(":", "").lower(), baud) + super(Jkbms_Ble, self).__init__(address.replace(":", "").lower(), baud, address) self.type = self.BATTERYTYPE self.jk = JkBmsBle(address) @@ -43,10 +43,10 @@ def test_connection(self): logger.error("no BMS found at " + self.jk.address) return False - + """ # before indipended service, has to be checked - + logger.info("test of jkbmsble") tries = 0 while True: @@ -165,7 +165,7 @@ 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 + if st["cell_info"]["balancing_current"] < 32768 else ( 65536/1000 - st["cell_info"]["balancing_current"] ) * -1 ) self.balancing_action = st["cell_info"]["balancing_action"] @@ -174,7 +174,7 @@ def refresh_data(self): for c in range(self.cell_count): if self.balancing and ( st["cell_info"]["min_voltage_cell"] == c - or st["cell_info"]["max_voltage_cell"] == c + or st["cell_info"]["max_voltage_cell"] == c ): self.cells[c].balance = True else: diff --git a/etc/dbus-serialbattery/lifepower.py b/etc/dbus-serialbattery/lifepower.py index 90e31ce5..cc3058c0 100644 --- a/etc/dbus-serialbattery/lifepower.py +++ b/etc/dbus-serialbattery/lifepower.py @@ -3,14 +3,17 @@ from battery import Protection, Battery, Cell from utils import * from struct import * +import re class Lifepower(Battery): - def __init__(self, port, baud): - super(Lifepower, self).__init__(port, baud) + def __init__(self, port, baud, address): + super(Lifepower, self).__init__(port, baud, address) self.type = self.BATTERYTYPE command_general = b"\x7E\x01\x01\x00\xFE\x0D" + command_hardware_version = b"\x7E\x01\x42\x00\xFC\x0D" + command_firmware_version = b"\x7E\x01\x33\x00\xFE\x0D" balancing = 0 BATTERYTYPE = "EG4 Lifepower" LENGTH_CHECK = 5 @@ -27,10 +30,29 @@ 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_charge_current = MAX_BATTERY_CHARGE_CURRENT + self.max_battery_current = MAX_BATTERY_CURRENT self.max_battery_discharge_current = MAX_BATTERY_DISCHARGE_CURRENT - self.version = "EG4 BMS V1.0" - logger.info(self.hardware_version) + hardware_version = self.read_serial_data_eg4(self.command_hardware_version) + if hardware_version: + # I get some characters that I'm not able to figure out the encoding, probably chinese so I discard it + # Also remove any special character that is not printable or make no sense. + self.hardware_version = re.sub( + r"[^a-zA-Z0-9-._ ]", + "", + str(hardware_version, encoding="utf-8", errors="ignore"), + ) + logger.info("Hardware Version:" + self.hardware_version) + + version = self.read_serial_data_eg4(self.command_firmware_version) + if version: + self.version = re.sub( + r"[^a-zA-Z0-9-._ ]", "", str(version, encoding="utf-8", errors="ignore") + ) + logger.info("Firmware Version:" + self.version) + + # polling every second seems to create some error messages + # change to 2 seconds + self.poll_interval = 2000 return True def refresh_data(self): @@ -63,7 +85,6 @@ def read_status_data(self): for i in range(0, len(group_payload), 2) ] ) - i = end # Cells @@ -73,7 +94,10 @@ def read_status_data(self): self.cells = [Cell(True) for _ in range(0, self.cell_count)] for i, cell in enumerate(self.cells): - cell.voltage = groups[0][i] / 1000 + # there is a situation where the MSB bit of the high byte may come set + # I got that when I got a high voltage alarm from the unit. + # make sure that bit is 0, by doing an AND with 32767 (01111111 1111111) + cell.voltage = (groups[0][i] & 32767) / 1000 # Current self.current = (30000 - groups[1][0]) / 100 @@ -85,9 +109,13 @@ def read_status_data(self): self.capacity = groups[3][0] / 100 # Temperature - # TODO There is a significant amount of temperature information being ignored here - # see https://github.com/slim-bean/powermon#group-5 - self.temp1 = groups[4][0] - 50 + self.temp_sensors = 6 + self.temp1 = (groups[4][0] & 0xFF) - 50 + self.temp2 = (groups[4][1] & 0xFF) - 50 + self.temp3 = (groups[4][2] & 0xFF) - 50 + self.temp4 = (groups[4][3] & 0xFF) - 50 + self.temp5 = (groups[4][4] & 0xFF) - 50 + self.temp6 = (groups[4][5] & 0xFF) - 50 # Alarms # 4th bit: Over Current Protection @@ -106,11 +134,6 @@ def read_status_data(self): # Voltage self.voltage = groups[7][0] / 100 - - # TODO State of health - - self.hardware_version = "EG4 Lifepower " + str(self.cell_count) + " cells" - return True def get_balancing(self): diff --git a/etc/dbus-serialbattery/lltjbd.py b/etc/dbus-serialbattery/lltjbd.py index e8beb480..9af90c02 100644 --- a/etc/dbus-serialbattery/lltjbd.py +++ b/etc/dbus-serialbattery/lltjbd.py @@ -45,8 +45,8 @@ def set_software_lock(self, value): class LltJbd(Battery): - def __init__(self, port, baud): - super(LltJbd, self).__init__(port, baud) + def __init__(self, port, baud, address): + super(LltJbd, self).__init__(port, baud, address) self.protection = LltJbdProtection() self.type = self.BATTERYTYPE diff --git a/etc/dbus-serialbattery/mnb.py b/etc/dbus-serialbattery/mnb.py index 233d624a..8e91bd5b 100644 --- a/etc/dbus-serialbattery/mnb.py +++ b/etc/dbus-serialbattery/mnb.py @@ -48,7 +48,7 @@ def set_software_lock(self, value): class MNB(Battery): def __init__(self, port, baud, address=0): - super(MNB, self).__init__(port, baud) + super(MNB, self).__init__(port, baud, address) self.protection = MNBProtection() self.hardware_version = 1.02 self.voltage = 26 diff --git a/etc/dbus-serialbattery/reinstalllocal.sh b/etc/dbus-serialbattery/reinstalllocal.sh index 14eade66..b553ba6a 100644 --- a/etc/dbus-serialbattery/reinstalllocal.sh +++ b/etc/dbus-serialbattery/reinstalllocal.sh @@ -1,11 +1,12 @@ -#!/bin/sh +#!/bin/bash +set -x DRIVERNAME=dbus-serialbattery -#handle read only mounts +# handle read only mounts sh /opt/victronenergy/swupdate-scripts/remount-rw.sh -#install +# install rm -rf /opt/victronenergy/service/$DRIVERNAME rm -rf /opt/victronenergy/service-templates/$DRIVERNAME rm -rf /opt/victronenergy/$DRIVERNAME @@ -14,7 +15,15 @@ cp -f /data/etc/$DRIVERNAME/* /opt/victronenergy/$DRIVERNAME &>/dev/null cp -rf /data/etc/$DRIVERNAME/service /opt/victronenergy/service-templates/$DRIVERNAME sh /data/etc/$DRIVERNAME/installqml.sh -#restart if running +# check if serial-starter.d was deleted +serialstarter=/data/conf/serial-starter.d +if [ ! -f $serialstarter ]; then + echo "service sbattery dbus-serialbattery" >> $serialstarter + echo "alias default gps:vedirect:sbattery" >> $serialstarter + echo "alias rs485 cgwacs:fzsonick:imt:modbus:sbattery" >> $serialstarter +fi + +# restart if running pkill -f "python .*/$DRIVERNAME.py" # add install-script to rc.local to be ready for firmware update diff --git a/etc/dbus-serialbattery/renogy.py b/etc/dbus-serialbattery/renogy.py index 66ad7aa7..6f1c6c6f 100644 --- a/etc/dbus-serialbattery/renogy.py +++ b/etc/dbus-serialbattery/renogy.py @@ -6,7 +6,7 @@ class Renogy(Battery): def __init__(self, port, baud, address): - super(Renogy, self).__init__(port, baud) + super(Renogy, self).__init__(port, baud, address) self.type = self.BATTERYTYPE # The RBT100LFP12SH-G1 uses 0xF7, another battery uses 0x30 diff --git a/etc/dbus-serialbattery/restoregui.sh b/etc/dbus-serialbattery/restoregui.sh index bd521103..f26bf9cc 100644 --- a/etc/dbus-serialbattery/restoregui.sh +++ b/etc/dbus-serialbattery/restoregui.sh @@ -1,5 +1,5 @@ -#!/bin/sh - +#!/bin/bash +set -x #restore original backup cp -f /opt/victronenergy/gui/qml/PageBattery.qml.backup /opt/victronenergy/gui/qml/PageBattery.qml diff --git a/etc/dbus-serialbattery/revov.py b/etc/dbus-serialbattery/revov.py index 40b245a1..d3fc2563 100755 --- a/etc/dbus-serialbattery/revov.py +++ b/etc/dbus-serialbattery/revov.py @@ -18,8 +18,8 @@ class Revov(Battery): - def __init__(self, port, baud): - super(Revov, self).__init__(port, baud) + def __init__(self, port, baud, address): + super(Revov, self).__init__(port, baud, address) self.type = self.BATTERYTYPE self.soc = 100 self.voltage = None diff --git a/etc/dbus-serialbattery/sinowealth.py b/etc/dbus-serialbattery/sinowealth.py index 8788c60e..f5021ff5 100755 --- a/etc/dbus-serialbattery/sinowealth.py +++ b/etc/dbus-serialbattery/sinowealth.py @@ -5,8 +5,8 @@ class Sinowealth(Battery): - def __init__(self, port, baud): - super(Sinowealth, self).__init__(port, baud) + def __init__(self, port, baud, address): + super(Sinowealth, self).__init__(port, baud, address) self.poll_interval = 2000 self.type = self.BATTERYTYPE diff --git a/etc/dbus-serialbattery/start-serialbattery.sh b/etc/dbus-serialbattery/start-serialbattery.sh index c6f79742..38de1d58 100644 --- a/etc/dbus-serialbattery/start-serialbattery.sh +++ b/etc/dbus-serialbattery/start-serialbattery.sh @@ -1,5 +1,5 @@ #!/bin/bash -# +set -x . /opt/victronenergy/serial-starter/run-service.sh diff --git a/etc/dbus-serialbattery/uninstall.sh b/etc/dbus-serialbattery/uninstall.sh new file mode 100644 index 00000000..3d44bcec --- /dev/null +++ b/etc/dbus-serialbattery/uninstall.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -x + +DRIVERNAME=dbus-serialbattery + +# handle read only mounts +sh /opt/victronenergy/swupdate-scripts/remount-rw.sh + +# remove files +rm -f /data/conf/serial-starter.d +rm -rf /opt/victronenergy/service/$DRIVERNAME +rm -rf /opt/victronenergy/service-templates/$DRIVERNAME +rm -rf /opt/victronenergy/$DRIVERNAME +rm -rf /service/dbus-blebattery-* + +# kill if running +pkill -f "python .*/$DRIVERNAME.py" + +# remove install-script from rc.local +sed -i "/sh \/data\/etc\/$DRIVERNAME\/reinstalllocal.sh/d" /data/rc.local +sed -i "/sh \/data\/etc\/$DRIVERNAME\/installble.sh/d" /data/rc.local + +# remove cronjob +sed -i "/5 0,12 \* \* \* \/etc\/init.d\/bluetooth restart/d" /var/spool/cron/root diff --git a/etc/dbus-serialbattery/utils.py b/etc/dbus-serialbattery/utils.py index 28e644b3..b56904c0 100644 --- a/etc/dbus-serialbattery/utils.py +++ b/etc/dbus-serialbattery/utils.py @@ -1,5 +1,10 @@ # -*- coding: utf-8 -*- import logging + +import configparser +from pathlib import Path +from typing import List, Any, Callable + import serial from time import sleep from struct import unpack_from @@ -10,163 +15,250 @@ logger = logging.getLogger("SerialBattery") logger.setLevel(logging.INFO) +config = configparser.ConfigParser() +path = Path(__file__).parents[0] +default_config_file_path = path.joinpath("config.default.ini").absolute().__str__() +custom_config_file_path = path.joinpath("config.ini").absolute().__str__() +config.read([default_config_file_path, custom_config_file_path]) + + +def _get_list_from_config( + group: str, option: str, mapper: Callable[[Any], Any] = lambda v: v +) -> List[Any]: + rawList = config[group][option].split(",") + return list( + map(mapper, [item for item in rawList if item != "" and item is not None]) + ) + + # battery types # if not specified: baud = 9600 -battery_types = [ - {"bms": "LltJbd"}, - {"bms": "Ant", "baud": 19200}, - {"bms": "Daly", "address": b"\x40"}, - {"bms": "Daly", "address": b"\x80"}, - {"bms": "Jkbms", "baud": 115200}, - # {"bms" : "Sinowealth"}, - {"bms": "Lifepower"}, - {"bms": "Renogy", "address": b"\x30"}, - {"bms": "Renogy", "address": b"\xF7"}, - {"bms": "Ecs", "baud": 19200}, - # {"bms" : "MNB"}, -] # Constants - Need to dynamically get them in future -DRIVER_VERSION = '0.15' -DRIVER_SUBVERSION = ".0-ble" +DRIVER_VERSION = "1.0" +DRIVER_SUBVERSION = ".0-jkbms_ble (20230413)" zero_char = chr(48) degree_sign = "\N{DEGREE SIGN}" # Choose the mode for voltage / current limitations (True / False) -# False is a Step mode. This is the default with limitations on hard boundary steps -# True "Linear" # New linear limitations by WaldemarFech for smoother values -LINEAR_LIMITATION_ENABLE = False - -# -------- Cell Voltage limitation --------- -# Description: -# Maximal charge / discharge current will be in-/decreased depending on min- and max-cell-voltages -# Example: 18cells * 3.55V/cell = 63.9V max charge voltage. 18 * 2.7V = 48,6V min discharge voltage -# ... but the (dis)charge current will be (in-/)decreased, if even ONE SINGLE BATTERY CELL reaches the limits +# False is a step mode. This is the default with limitations on hard boundary steps +# True is a linear mode. For CCL and DCL the values between the steps are calculated for smoother values (by WaldemarFech) +# For CVL the penalties are only applied, if the cell voltage reaches the penalty voltage +LINEAR_LIMITATION_ENABLE = "True" == config["DEFAULT"]["LINEAR_LIMITATION_ENABLE"] + +# Battery Current limits +MAX_BATTERY_CHARGE_CURRENT = float(config["DEFAULT"]["MAX_BATTERY_CHARGE_CURRENT"]) +MAX_BATTERY_DISCHARGE_CURRENT = float(config["DEFAULT"]["MAX_BATTERY_DISCHARGE_CURRENT"]) + + +# --------- Charge Voltage limitation (affecting CVL) --------- +# Description: Limit max charging voltage (MAX_CELL_VOLTAGE * cell count) and switch from max voltage to float voltage (FLOAT_CELL_VOLTAGE * cell count) +# after max voltage is reached for MAX_VOLTAGE_TIME_SEC. It switches back to max voltage after SoC is below SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT +# If LINEAR_LIMITATION_ENABLE is set to True then penalty voltages are applied +# Example: 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 90% +# Charge voltage control management enable (True/False). +CVCM_ENABLE = "True" == config["DEFAULT"]["CVCM_ENABLE"] + +# -- 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 +MIN_CELL_VOLTAGE = float(config["DEFAULT"]["MIN_CELL_VOLTAGE"]) +MAX_CELL_VOLTAGE = float(config["DEFAULT"]["MAX_CELL_VOLTAGE"]) +# Max voltage can seen as absorption voltage +FLOAT_CELL_VOLTAGE = float(config["DEFAULT"]["FLOAT_CELL_VOLTAGE"]) + +# -- Penalty Voltages +# NOTE: works only when LINEAR_LIMITATION_ENABLE = True +# More details can be found here: https://github.com/Louisvdw/dbus-serialbattery/issues/297#issuecomment-1327142635 +# If the cell voltage reaches 3.48V, then reduce actual battery-voltage by 0.01V +# If the cell voltage goes over 3.6V, then the maximum penalty will not be exceeded +# There will be a sum of all penalties for each cell, which exceeds the limits +# NOTE: The first value of PENALTY_AT_CELL_VOLTAGE has to be at least MAX_CELL_VOLTAGE + the first value of PENALTY_BATTERY_VOLTAGE, +# else the FLOAT_CELL_VOLTAGE is never set. Additionally the battery voltage has to reach max voltage and all cells has to be below penalty voltage to switch to float voltage. +PENALTY_AT_CELL_VOLTAGE = _get_list_from_config( + "DEFAULT", + "PENALTY_AT_CELL_VOLTAGE", + lambda v: float(v) +) +# this voltage will be subtracted +PENALTY_BATTERY_VOLTAGE = _get_list_from_config( + "DEFAULT", + "PENALTY_BATTERY_VOLTAGE", + lambda v: float(v) +) +# Specify in seconds how often the penalty should be recalculated +PENALTY_RECALCULATE_EVERY = int(config["DEFAULT"]["PENALTY_RECALCULATE_EVERY"]) + +# -- CVL Reset based on SoC option +# Reset max voltage after +MAX_VOLTAGE_TIME_SEC = float(config["DEFAULT"]["MAX_VOLTAGE_TIME_SEC"]) +# Specify SoC where CVL limit is reset to max voltage +SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT = float(config["DEFAULT"]["SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT"]) + + +# --------- 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 # Charge current control management referring to cell-voltage enable (True/False). -CCCM_CV_ENABLE = True +CCCM_CV_ENABLE = "True" == config["DEFAULT"]["CCCM_CV_ENABLE"] # Discharge current control management referring to cell-voltage enable (True/False). -DCCM_CV_ENABLE = True - -# Set Steps to reduce battery current. The current will be changed linear between those steps -CELL_VOLTAGES_WHILE_CHARGING = [3.55, 3.50, 3.45, 3.30] -MAX_CHARGE_CURRENT_CV = [0, 2, 30, 60] - -CELL_VOLTAGES_WHILE_DISCHARGING = [2.70, 2.80, 2.90, 3.10] -MAX_DISCHARGE_CURRENT_CV = [0, 5, 30, 60] - -# -------- Temperature limitation --------- -# Description: -# Maximal charge / discharge current will be in-/decreased depending on temperature +DCCM_CV_ENABLE = "True" == config["DEFAULT"]["DCCM_CV_ENABLE"] + +# 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 = _get_list_from_config( + "DEFAULT", "CELL_VOLTAGES_WHILE_CHARGING", lambda v: float(v) +) +MAX_CHARGE_CURRENT_CV = _get_list_from_config( + "DEFAULT", + "MAX_CHARGE_CURRENT_CV_FRACTION", + lambda v: MAX_BATTERY_CHARGE_CURRENT * float(v), +) + +CELL_VOLTAGES_WHILE_DISCHARGING = _get_list_from_config( + "DEFAULT", "CELL_VOLTAGES_WHILE_DISCHARGING", lambda v: float(v) +) +MAX_DISCHARGE_CURRENT_CV = _get_list_from_config( + "DEFAULT", + "MAX_DISCHARGE_CURRENT_CV_FRACTION", + lambda v: MAX_BATTERY_DISCHARGE_CURRENT * float(v), +) + + +# --------- 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. # Charge current control management referring to temperature enable (True/False). -CCCM_T_ENABLE = True +CCCM_T_ENABLE = "True" == config["DEFAULT"]["CCCM_T_ENABLE"] # Charge current control management referring to temperature enable (True/False). -DCCM_T_ENABLE = True - -# Set Steps to reduce battery current. The current will be changed linear between those steps -TEMPERATURE_LIMITS_WHILE_CHARGING = [55, 40, 35, 5, 2, 0] -MAX_CHARGE_CURRENT_T = [0, 28, 60, 60, 28, 0] - -TEMPERATURE_LIMITS_WHILE_DISCHARGING = [55, 40, 35, 5, 0, -20] -MAX_DISCHARGE_CURRENT_T = [0, 28, 60, 60, 28, 0] - -# if the cell voltage reaches 3.55V, then reduce current battery-voltage by 0.01V -# if the cell voltage goes over 3.6V, then the maximum penalty will not be exceeded -# there will be a sum of all penalties for each cell, which exceeds the limits -PENALTY_AT_CELL_VOLTAGE = [3.45, 3.55, 3.6] -PENALTY_BATTERY_VOLTAGE = [0.01, 1.0, 2.0] # this voltage will be subtracted - - -# -------- SOC limitation --------- -# Description: -# Maximal charge / discharge current will be increased / decreased depending on State of Charge, see CC_SOC_LIMIT1 etc. -# The State of Charge (SoC) charge / discharge current will be in-/decreased depending on SOC. -# Example: 16cells * 3.45V/cell = 55,2V max charge voltage. 16*2.9V = 46,4V min discharge voltage -# Cell min/max voltages - used with the cell count to get the min/max battery voltage -MIN_CELL_VOLTAGE = 2.9 -MAX_CELL_VOLTAGE = 3.45 -FLOAT_CELL_VOLTAGE = 3.35 -MAX_VOLTAGE_TIME_SEC = 15 * 60 -SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT = 90 - -# battery Current limits -MAX_BATTERY_CHARGE_CURRENT = 50.0 -MAX_BATTERY_DISCHARGE_CURRENT = 60.0 - +DCCM_T_ENABLE = "True" == config["DEFAULT"]["DCCM_T_ENABLE"] + +# 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 = _get_list_from_config( + "DEFAULT", + "TEMPERATURE_LIMITS_WHILE_CHARGING", + lambda v: float(v) +) +MAX_CHARGE_CURRENT_T = _get_list_from_config( + "DEFAULT", + "MAX_CHARGE_CURRENT_T_FRACTION", + 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) +) +MAX_DISCHARGE_CURRENT_T = _get_list_from_config( + "DEFAULT", + "MAX_DISCHARGE_CURRENT_T_FRACTION", + lambda v: MAX_BATTERY_DISCHARGE_CURRENT * float(v), +) + + +# --------- 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. # Charge current control management enable (True/False). -CCCM_SOC_ENABLE = True +CCCM_SOC_ENABLE = "True" == config["DEFAULT"]["CCCM_SOC_ENABLE"] # 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 = 5 -CC_CURRENT_LIMIT2 = MAX_BATTERY_CHARGE_CURRENT / 4 -CC_CURRENT_LIMIT3 = MAX_BATTERY_CHARGE_CURRENT / 2 - -# discharge current soc limits -DC_SOC_LIMIT1 = 10 -DC_SOC_LIMIT2 = 20 -DC_SOC_LIMIT3 = 30 +DCCM_SOC_ENABLE = "True" == config["DEFAULT"]["DCCM_SOC_ENABLE"] + +# Charge current soc limits +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"]) + +# Charge current limits +CC_CURRENT_LIMIT1 = MAX_BATTERY_CHARGE_CURRENT * float(config["DEFAULT"]["CC_CURRENT_LIMIT1_FRACTION"]) +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"]) + +# Discharge current soc limits +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"]) + +# Discharge current limits +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"]) +DC_CURRENT_LIMIT3 = MAX_BATTERY_DISCHARGE_CURRENT * float(config["DEFAULT"]["DC_CURRENT_LIMIT3_FRACTION"]) + + +# --------- 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 +# 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 +# Leave empty to disable +TIME_TO_SOC_POINTS = _get_list_from_config("DEFAULT", "TIME_TO_SOC_POINTS", lambda v: int(v)) +# Specify TimeToSoc value type [Valid values 1, 2, 3] +# 1 Seconds +# 2 Time string d h m s +# 3 Both seconds and time string " [d h m s]" +TIME_TO_SOC_VALUE_TYPE = int(config["DEFAULT"]["TIME_TO_SOC_VALUE_TYPE"]) +# Specify in seconds how often the TimeToSoc should be recalculated +# Minimum are 5 seconds to prevent CPU overload +TIME_TO_SOC_RECALCULATE_EVERY = int(config["DEFAULT"]["TIME_TO_SOC_RECALCULATE_EVERY"]) if int(config["DEFAULT"]["TIME_TO_SOC_RECALCULATE_EVERY"]) > 5 else 5 +# Include TimeToSoC points when moving away from the SoC point [Valid values True, False] +# These will be as negative time. Disabling this improves performance slightly +TIME_TO_SOC_INC_FROM = "True" == config["DEFAULT"]["TIME_TO_SOC_INC_FROM"] + + +# --------- Additional settings --------- +# Specify only one BMS type to load else leave empty to try to load all availabe +# LltJbd, Ant, Daly, Daly, Jkbms, Lifepower, Renogy, Renogy, Ecs +BMS_TYPE = config["DEFAULT"]["BMS_TYPE"] + +# Publish the config settings to the dbus path "/Info/Config/" +PUBLISH_CONFIG_VALUES = int(config["DEFAULT"]["PUBLISH_CONFIG_VALUES"]) + +# 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) +# 1 Format: /Voltages/Cell (also available for display on Remote Console) +# 2 Format: /Cell/#/Volts +# 3 Both formats 1 and 2 +BATTERY_CELL_DATA_FORMAT = int(config["DEFAULT"]["BATTERY_CELL_DATA_FORMAT"]) -# discharge current limits -DC_CURRENT_LIMIT1 = 5 -DC_CURRENT_LIMIT2 = MAX_BATTERY_DISCHARGE_CURRENT / 4 -DC_CURRENT_LIMIT3 = MAX_BATTERY_DISCHARGE_CURRENT / 2 +# Simulate Midpoint graph (True/False). +MIDPOINT_ENABLE = "True" == config["DEFAULT"]["MIDPOINT_ENABLE"] -# Charge voltage control management enable (True/False). -CVCM_ENABLE = False -# Simulate Midpoint graph (True/False). -MIDPOINT_ENABLE = False +# --------- BMS specific settings --------- -# soc low levels -SOC_LOW_WARNING = 20 -SOC_LOW_ALARM = 10 +# -- 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 +SOC_LOW_WARNING = float(config["DEFAULT"]["SOC_LOW_WARNING"]) +SOC_LOW_ALARM = float(config["DEFAULT"]["SOC_LOW_ALARM"]) -# Daly settings +# -- Daly settings # Battery capacity (amps) if the BMS does not support reading it -BATTERY_CAPACITY = 50 +BATTERY_CAPACITY = float(config["DEFAULT"]["BATTERY_CAPACITY"]) # Invert Battery Current. Default non-inverted. Set to -1 to invert -INVERT_CURRENT_MEASUREMENT = 1 - -# TIME TO SOC settings [Valid values 0-100, but I don't recommend more that 20 intervals] -# Set of SoC percentages to report on dbus. The more you specify the more it will impact system performance. -# TIME_TO_SOC_POINTS = [100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, 10, 5, 0] -# Every 5% SoC -# TIME_TO_SOC_POINTS = [100, 95, 90, 85, 75, 50, 25, 20, 10, 0] -TIME_TO_SOC_POINTS = [] # No data set to disable -# Specify TimeToSoc value type: [Valid values 1,2,3] -# TIME_TO_SOC_VALUE_TYPE = 1 # Seconds -# TIME_TO_SOC_VALUE_TYPE = 2 # Time string HH:MN:SC -TIME_TO_SOC_VALUE_TYPE = 3 # Both Seconds and time str " [days, HR:MN:SC]" -# Specify how many loop cycles between each TimeToSoc updates -TIME_TO_SOC_LOOP_CYCLES = 5 -# Include TimeToSoC points when moving away from the SoC point. [Valid values True,False] -# These will be as negative time. Disabling this improves performance slightly. -TIME_TO_SOC_INC_FROM = False - - -# 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) -# 1 Format: /Voltages/Cell# (also available for display on Remote Console) -# 2 Format: /Cell/#/Volts -# 3 Both formats 1 and 2 -BATTERY_CELL_DATA_FORMAT = 1 +INVERT_CURRENT_MEASUREMENT = int(config["DEFAULT"]["INVERT_CURRENT_MEASUREMENT"]) -# Settings for ESC GreenMeter and Lipro devices -GREENMETER_ADDRESS = 1 -LIPRO_START_ADDRESS = 2 -LIPRO_END_ADDRESS = 4 -LIPRO_CELL_COUNT = 15 +# -- ESC GreenMeter and Lipro device settings +GREENMETER_ADDRESS = int(config["DEFAULT"]["GREENMETER_ADDRESS"]) +LIPRO_START_ADDRESS = int(config["DEFAULT"]["LIPRO_START_ADDRESS"]) +LIPRO_END_ADDRESS = int(config["DEFAULT"]["LIPRO_END_ADDRESS"]) +LIPRO_CELL_COUNT = int(config["DEFAULT"]["LIPRO_CELL_COUNT"]) + +# --------- Functions --------- def constrain(val, min_val, max_val): if min_val > max_val: min_val, max_val = max_val, min_val @@ -267,7 +359,12 @@ def open_serial_port(port, baud): # Read data from previously openned serial port def read_serialport_data( - ser, command, length_pos, length_check, length_fixed=None, length_size=None + ser: serial.Serial, + command, + length_pos, + length_check, + length_fixed=None, + length_size=None, ): try: ser.flushOutput() @@ -330,3 +427,19 @@ def read_serialport_data( except serial.SerialException as e: logger.error(e) return False + + +locals_copy = locals().copy() + + +def publish_config_variables(dbusservice): + for variable, value in locals_copy.items(): + if variable.startswith("__"): + continue + if ( + isinstance(value, float) + or isinstance(value, int) + or isinstance(value, str) + or isinstance(value, List) + ): + dbusservice.add_path(f"/Info/Config/{variable}", value) diff --git a/requirements.txt b/requirements.txt index b8b16d03..8bec2184 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ pyserial==3.5 -velib_python==2.80 minimalmodbus==2.0.1