diff --git a/CHANGELOG.md b/CHANGELOG.md
index 30822f96..d3c14b0b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,10 +12,12 @@
* Added: Balancing switch status to the GUI -> SerialBattery -> IO by @mr-manuel
* Added: Block charge/discharge when BMS communication is lost. Can be enabled trough the config file by @mr-manuel
* Added: Charge Mode display by @mr-manuel
+* Added: Check minimum required Venus OS version before installing by @mr-manuel
* Added: Choose how battery temperature is assembled (mean temp 1 & 2, only temp 1 or only temp 2) by @mr-manuel
* Added: Config file by @ppuetsch
* Added: Create empty `config.ini` for easier user usage by @mr-manuel
* Added: Cronjob to restart Bluetooth service every 12 hours by @mr-manuel
+* Added: Daly BMS - Discharge / Charge Mosfet switching over remote console/GUI https://github.com/Louisvdw/dbus-serialbattery/issues/26 by @transistorgit
* Added: Daly BMS - Read capacity https://github.com/Louisvdw/dbus-serialbattery/pull/594 by @transistorgit
* Added: Daly BMS - Read production date and build unique identifier by @transistorgit
* Added: Daly BMS - Set SoC by @transistorgit
@@ -23,7 +25,9 @@
* Added: Device name field (found in the GUI -> SerialBattery -> Device), that show a custom string that can be set in some BMS, if available by @mr-manuel
* Added: Driver uninstall script by @mr-manuel
* Added: Fix for Venus OS >= v3.00~14 showing unused items https://github.com/Louisvdw/dbus-serialbattery/issues/469 by @mr-manuel
+* Added: HeltecSmartBMS driver by @ramack
* Added: HighInternalTemperature alarm (MOSFET) for JKBMS by @mr-manuel
+* Added: HLPdata BMS driver by @ peterohman
* Added: Improved maintainability (flake8, black lint), introduced code checks and automate release build https://github.com/Louisvdw/dbus-serialbattery/pull/386 by @ppuetsch
* Added: Install needed Bluetooth components automatically after a Venus OS upgrade by @mr-manuel
* Added: JKBMS - MOS temperature https://github.com/Louisvdw/dbus-serialbattery/pull/440 by @baphomett
@@ -37,12 +41,16 @@
* Added: JKBMS BLE - Show if balancing is active and which cells are balancing by @mr-manuel
* Added: JKBMS BLE - Show serial number and "User Private Data" field that can be set in the JKBMS App to identify the BMS in a multi battery environment by @mr-manuel
* Added: JKBMS BLE driver by @baranator
+* Added: LLT/JBD BMS BLE driver by @idstein
* Added: Possibility to add `config.ini` to the root of a USB flash drive on install via the USB method by @mr-manuel
+* Added: Possibility to configure a `VOLTAGE_DROP` voltage, if you are using a SmartShunt as battery monitor as there is a little voltage difference https://github.com/Louisvdw/dbus-serialbattery/discussions/632 by @mr-manuel
* Added: Post install notes by @mr-manuel
* Added: Read charge/discharge limits from JKBMS by @mr-manuel
* Added: Recalculation interval in linear mode for CVL, CCL and DCL by @mr-manuel
+* Added: Rename TAR file after USB/SD card install to not overwrite the data on every reboot https://github.com/Louisvdw/dbus-serialbattery/issues/638 by @mr-manuel
* Added: Reset values to None, if battery goes offline (not reachable for 10s). Fixes https://github.com/Louisvdw/dbus-serialbattery/issues/193 https://github.com/Louisvdw/dbus-serialbattery/issues/64 by @transistorgit
* Added: Script to install directly from repository by @mr-manuel
+* Added: Seplos BMS driver by @wollew
* Added: Serial number field (found in the GUI -> SerialBattery -> Device), that show the serial number or a unique identifier for the BMS, if available by @mr-manuel
* Added: Show charge mode (absorption, bulk, ...) in Parameters page by @mr-manuel
* Added: Show charge/discharge limitation reason by @mr-manuel
@@ -52,11 +60,15 @@
* Added: Show TimeToGo in GUI only, if enabled by @mr-manuel
* Added: Support for HLPdata BMS4S https://github.com/Louisvdw/dbus-serialbattery/pull/505 by @peterohman
* Added: Support for Seplos BMS https://github.com/Louisvdw/dbus-serialbattery/pull/530 by @wollew
+* Added: Temperature 1-4 are now also available on the dbus and MQTT by @idstein
* Added: Temperature name for temperature sensor 1 & 2. This allows to see which sensor is low and high (e.g. battery and cable) by @mr-manuel
* Changed: `reinstall-local.sh` to recreate `/data/conf/serial-starter.d`, if deleted by `disable.sh` --> to check if the file `conf/serial-starter.d` could now be removed from the repository by @mr-manuel
* Changed: Added QML to `restore-gui.sh` by @mr-manuel
* Changed: Bash output by @mr-manuel
+* Changed: CVL calculation improvement. Removed cell voltage penalty. Replaced by automatic voltage calculation. Max voltage is kept until cells are balanced and reset when cells are inbalanced or SoC is below threshold by @mr-manuel
+* Changed: Daly BMS - Fixed BMS alerts by @mr-manuel
* Changed: Daly BMS - Improved driver stability by @transistorgit & @mr-manuel
+* Changed: Daly BMS - Reworked serial parser by @transistorgit
* Changed: Default config file by @ppuetsch
* Added missing descriptions to make it much clearer to understand by @mr-manuel
* Changed name from `default_config.ini` to `config.default.ini` https://github.com/Louisvdw/dbus-serialbattery/pull/412#issuecomment-1434287942 by @mr-manuel
@@ -67,14 +79,17 @@
* Changed: Default LINEAR_LIMITATION_ENABLE from False to True by @mr-manuel
* 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 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
@@ -83,7 +98,6 @@
* Changed: Moved Bluetooth part to `reinstall-local.sh` by @mr-manuel
* Changed: Moved BMS scripts to subfolder by @mr-manuel
* Changed: Removed all wildcard imports and fixed black lint errors by @mr-manuel
-* Changed: Removed cell voltage penalty. Replaced by automatic voltage calculation. Max voltage is kept until cells are balanced and reset when cells are inbalanced by @mr-manuel
* Changed: Renamed scripts for better reading #532 by @mr-manuel
* Changed: Reworked and optimized installation scripts by @mr-manuel
* Changed: Separate Time-To-Go and Time-To-SoC activation by @mr-manuel
diff --git a/README.md b/README.md
index 063eaaa3..38f11e44 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,19 @@
# dbus-serialbattery
This is a driver for Venus OS devices (any GX device sold by Victron or a Raspberry Pi running the Venus OS image).
-The driver will communicate with a Battery Management System (BMS) that support serial communication (RS232, RS485 or TTL UART) and publish this data to the Venus OS system. The main purpose is to act as a Battery Monitor in your GX and supply State Of Charge (SOC) and other values to the inverter.
+The driver will communicate with a Battery Management System (BMS) that support serial (RS232, RS485 or TTL UART) and Bluetooth communication (see [BMS feature comparison](https://louisvdw.github.io/dbus-serialbattery/general/features#bms-feature-comparison) for details). The data is then published to the Venus OS system (dbus). The main purpose is to act as a Battery Monitor in your GX and supply State of Charge (SoC) and other values to the inverter/charger.
- * [BMS Types supported](https://louisvdw.github.io/dbus-serialbattery/general/supported-bms)
- * [FAQ](https://louisvdw.github.io/dbus-serialbattery/faq/)
- * [Features](https://louisvdw.github.io/dbus-serialbattery/general/features)
- * [How to install](https://louisvdw.github.io/dbus-serialbattery/general/install)
- * [How to troubleshoot](https://louisvdw.github.io/dbus-serialbattery/troubleshoot/)
+## Documentation
+
+* [Introduction](https://louisvdw.github.io/dbus-serialbattery/)
+* [Features](https://louisvdw.github.io/dbus-serialbattery/general/features)
+* [Supported BMS](https://louisvdw.github.io/dbus-serialbattery/general/supported-bms)
+* [How to install, update, disable, enable and uninstall](https://louisvdw.github.io/dbus-serialbattery/general/install)
+* [How to troubleshoot](https://louisvdw.github.io/dbus-serialbattery/troubleshoot/)
+* [FAQ](https://louisvdw.github.io/dbus-serialbattery/faq/)
### Supporting this project
-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.
+If you find this driver helpful please consider supporting this project. You can buy me a Ko-Fi or get in contact, if you would like to donate hardware for development.
### Support [Louisvdw](https://github.com/Louisvdw)
* Main developer
@@ -25,6 +28,7 @@ If you find this driver helpful please considder supporting this project. You ca
[
](https://www.paypal.com/donate/?hosted_button_id=3NEVZBDM5KABW)
+
### 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,
diff --git a/docs/docs/general/features.md b/docs/docs/general/features.md
index a7b2434c..2ed2e9b3 100644
--- a/docs/docs/general/features.md
+++ b/docs/docs/general/features.md
@@ -83,7 +83,7 @@ CCCM limits the charge/discharge current depending on the highest/lowest cell vo
* between `2.8V - 2.9V` → `5A `discharge
* below `<= 2.70V` → `0A` discharge
-### Temprature
+### Temperature
* `CCCM_T_ENABLE = True/False`
* `DCCM_T_ENABLE = True/False`
@@ -121,27 +121,27 @@ If the `MAX_CELL_VOLTAGE` \* `cell count` is reached for `MAX_VOLTAGE_TIME_SEC`
## BMS feature comparison
-| Feature | Ant | Daly | ECS | HLPdataBMS4S | JK BMS | Life/Tian Power | LLT/JBD | MNB (1) | Renogy | Seplos | Sinowealth (1) |
-| ---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
-| Voltage | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
-| Current | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
-| Power | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
-| State Of Charge | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
-| Battery temperature | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
-| MOSFET temperature | No | No | No | No | Yes | No | Yes | No | No | No | No |
-| Consumed Ah | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
-| Time-to-go | Calc | Calc | Calc | Calc | Calc | Calc | Calc | Calc | Calc | Calc | Calc |
-| Min/max cell voltages | Yes | Yes | No | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes |
-| Min/max temperature | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
-| Installed capacity | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
-| Available capacity | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
-| Cell details | No | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes | Yes | ? |
-| Balancing status | Yes | No | Yes | No | Yes | Yes | No | No | No | No | ? |
-| Raise alarms from the BMS | Yes | Yes | Yes (2) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | ? |
-| History of charge cycles | Yes | Yes | No | No | Yes | Yes | Yes | No | Yes | Yes | Yes |
-| Get CCL/DCL from the BMS | No | No | No | No | Yes | No | No | No | No | No | No |
-| Charge current control management (CCCM) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
-| Set battery parameters (DVCC) | Calc | Calc | Yes | Yes | Calc | Calc | Calc | Yes | Calc | Calc | Calc |
+| Feature | Ant | Daly | ECS | Heltec | HLPdataBMS4S | JK BMS | Life/Tian Power | LLT/JBD | MNB (1) | Renogy | Seplos | Sinowealth (1) |
+| ---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
+| Voltage | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
+| Current | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
+| Power | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
+| State Of Charge | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
+| Battery temperature | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
+| MOSFET temperature | No | No | No | Yes | No | Yes | No | Yes | No | No | No | No |
+| Consumed Ah | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
+| Time-to-go | Calc | Calc | Calc | Calc | Calc | Calc | Calc | Calc | Calc | Calc | Calc | Calc |
+| Min/max cell voltages | Yes | Yes | No | Yes | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes |
+| Min/max temperature | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
+| Installed capacity | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
+| Available capacity | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
+| Cell details | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes | Yes | ? |
+| Balancing status | Yes | No | Yes | Yes | No | Yes | Yes | No | No | No | No | ? |
+| Raise alarms from the BMS | Yes | Yes | Yes (2) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | ? |
+| History of charge cycles | Yes | Yes | No | No | No | Yes | Yes | Yes | No | Yes | Yes | Yes |
+| Get CCL/DCL from the BMS | No | No | No | Yes | No | Yes | No | No | No | No | No | No |
+| Charge current control management (CCCM) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
+| Set battery parameters (DVCC) | Calc | Calc | Yes | Calc | Yes | Calc | Calc | Calc | Yes | Calc | Calc | Calc |
`Calc` means that the value is calculated by the driver.
diff --git a/docs/docs/general/install.md b/docs/docs/general/install.md
index 56c891bc..4b30e030 100644
--- a/docs/docs/general/install.md
+++ b/docs/docs/general/install.md
@@ -122,7 +122,7 @@ Select `2` for `nightly build` and then select the branch you want to install fr
### BMS specific settings
* ECS BMS → https://github.com/Louisvdw/dbus-serialbattery/issues/254#issuecomment-1275924313
-
+* HeltecModbus → in case the modbus slave address of the BMS was adjusted from the factory default, configure the slave addresses to query in config.ini:HELTEC_MODBUS_ADDR. As always the battery settings shall be configured in the BMS already via app or computer.
## How to change the default limits
diff --git a/docs/docs/general/supported-bms.md b/docs/docs/general/supported-bms.md
index 416e7362..ff247950 100644
--- a/docs/docs/general/supported-bms.md
+++ b/docs/docs/general/supported-bms.md
@@ -22,6 +22,9 @@ Disabled by default since driver version `v0.14.0` as it causes other issues. Se
### • ECS GreenMeter with LiPro
+### • HeltecModbus SmartBMS (YanYang BMS)
+Communication to the Heltec SmartBMS (which is a rebranded YYBMS) via Modbus/RS485.
+
### • HLPdataBMS4S
### • [JKBMS](https://www.jkbms.com/products/) / Heltec BMS
diff --git a/etc/dbus-serialbattery/battery.py b/etc/dbus-serialbattery/battery.py
index 108cf211..eb5b2e2d 100644
--- a/etc/dbus-serialbattery/battery.py
+++ b/etc/dbus-serialbattery/battery.py
@@ -96,6 +96,8 @@ def init_values(self):
self.temp_sensors = None
self.temp1 = None
self.temp2 = None
+ self.temp3 = None
+ self.temp4 = None
self.temp_mos = None
self.cells: List[Cell] = []
self.control_charging = None
@@ -127,6 +129,15 @@ def test_connection(self) -> bool:
# return false when failed, true if successful
return False
+ def connection_name(self) -> str:
+ return "Serial " + self.port
+
+ def custom_name(self) -> str:
+ return "SerialBattery(" + self.type + ")"
+
+ def product_name(self) -> str:
+ return "SerialBattery(" + self.type + ")"
+
@abstractmethod
def get_settings(self) -> bool:
"""
@@ -164,6 +175,10 @@ def to_temp(self, sensor: int, value: float) -> None:
self.temp1 = min(max(value, -20), 100)
if sensor == 2:
self.temp2 = min(max(value, -20), 100)
+ if sensor == 3:
+ self.temp3 = min(max(value, -20), 100)
+ if sensor == 4:
+ self.temp4 = min(max(value, -20), 100)
def manage_charge_voltage(self) -> None:
"""
@@ -207,18 +222,21 @@ def manage_charge_voltage_linear(self) -> None:
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 (
- utils.MAX_CELL_VOLTAGE * self.cell_count <= voltageSum
+ (utils.MAX_CELL_VOLTAGE * self.cell_count) - utils.VOLTAGE_DROP
+ <= voltageSum
and voltageDiff
<= utils.CELL_VOLTAGE_DIFF_KEEP_MAX_VOLTAGE_UNTIL
and self.allow_max_voltage
):
self.max_voltage_start_time = time()
+
+ # allow max voltage again, if cells are unbalanced or SoC threshold is reached
elif (
- # utils.SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT > self.soc
- voltageDiff >= utils.CELL_VOLTAGE_DIFF_TO_RESET_VOLTAGE_LIMIT
- and not self.allow_max_voltage
- ):
+ utils.SOC_LEVEL_TO_RESET_VOLTAGE_LIMIT > self.soc
+ or voltageDiff >= utils.CELL_VOLTAGE_DIFF_TO_RESET_VOLTAGE_LIMIT
+ ) and not self.allow_max_voltage:
self.allow_max_voltage = True
else:
tDiff = time() - self.max_voltage_start_time
@@ -238,11 +256,14 @@ def manage_charge_voltage_linear(self) -> None:
):
self.linear_cvl_last_set = int(time())
- # Keep penalty above min battery voltage
+ # Keep penalty above min battery voltage and below max battery voltage
self.control_voltage = round(
- max(
- voltageSum - penaltySum,
- utils.MIN_CELL_VOLTAGE * self.cell_count,
+ min(
+ max(
+ voltageSum - penaltySum,
+ utils.MIN_CELL_VOLTAGE * self.cell_count,
+ ),
+ utils.MAX_CELL_VOLTAGE * self.cell_count,
),
3,
)
@@ -309,9 +330,8 @@ def manage_charge_voltage_step(self) -> None:
if self.max_voltage_start_time is None:
# check if max voltage is reached and start timer to keep max voltage
if (
- utils.MAX_CELL_VOLTAGE * self.cell_count <= voltageSum
- and self.allow_max_voltage
- ):
+ utils.MAX_CELL_VOLTAGE * self.cell_count
+ ) - utils.VOLTAGE_DROP <= voltageSum and self.allow_max_voltage:
# example 2
self.max_voltage_start_time = time()
@@ -802,14 +822,10 @@ def get_balancing(self) -> int:
return 1
return 0
- def extract_from_temp_values(self, extractor) -> Union[float, None]:
- if self.temp1 is not None and self.temp2 is not None:
- 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:
- return self.temp2
- else:
+ 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]:
@@ -818,46 +834,93 @@ def get_temp(self) -> Union[float, None]:
return self.temp1
elif utils.TEMP_BATTERY == 2:
return self.temp2
+ elif utils.TEMP_BATTERY == 3:
+ return self.temp3
+ elif utils.TEMP_BATTERY == 4:
+ return self.temp4
else:
- return self.extract_from_temp_values(
- extractor=lambda temp1, temp2: round(
- (float(temp1) + float(temp2)) / 2, 2
- )
- )
+ temps = [
+ t
+ for t in [self.temp1, self.temp2, self.temp3, self.temp4]
+ if t is not None
+ ]
+ n = len(temps)
+ if not temps or n == 0:
+ return None
+ data = sorted(temps)
+ if n % 2 == 1:
+ return data[n // 2]
+ else:
+ i = n // 2
+ return (data[i - 1] + data[i]) / 2
except TypeError:
return None
def get_min_temp(self) -> Union[float, None]:
try:
- return self.extract_from_temp_values(
- extractor=lambda temp1, temp2: min(temp1, temp2)
- )
+ temps = [
+ t
+ for t in [self.temp1, self.temp2, self.temp3, self.temp4]
+ if t is not None
+ ]
+ if not temps:
+ return None
+ return min(temps)
except TypeError:
return None
def get_min_temp_id(self) -> Union[str, None]:
try:
- if self.temp1 < self.temp2:
+ temps = [
+ (t, i)
+ for i, t in enumerate([self.temp1, self.temp2, self.temp3, self.temp4])
+ if t is not None
+ ]
+ if not temps:
+ return None
+ index = min(temps)[1]
+ if index == 0:
return utils.TEMP_1_NAME
- else:
+ if index == 1:
return utils.TEMP_2_NAME
+ if index == 2:
+ return utils.TEMP_3_NAME
+ if index == 3:
+ return utils.TEMP_4_NAME
except TypeError:
return None
def get_max_temp(self) -> Union[float, None]:
try:
- return self.extract_from_temp_values(
- extractor=lambda temp1, temp2: max(temp1, temp2)
- )
+ temps = [
+ t
+ for t in [self.temp1, self.temp2, self.temp3, self.temp4]
+ if t is not None
+ ]
+ if not temps:
+ return None
+ return max(temps)
except TypeError:
return None
def get_max_temp_id(self) -> Union[str, None]:
try:
- if self.temp1 > self.temp2:
+ temps = [
+ (t, i)
+ for i, t in enumerate([self.temp1, self.temp2, self.temp3, self.temp4])
+ if t is not None
+ ]
+ if not temps:
+ return None
+ index = max(temps)[1]
+ if index == 0:
return utils.TEMP_1_NAME
- else:
+ if index == 1:
return utils.TEMP_2_NAME
+ if index == 2:
+ return utils.TEMP_3_NAME
+ if index == 3:
+ return utils.TEMP_4_NAME
except TypeError:
return None
@@ -928,3 +991,12 @@ def log_settings(self) -> None:
def reset_soc_callback(self, path, value):
# callback for handling reset soc request
return
+
+ def force_charging_off_callback(self, path, value):
+ return
+
+ def force_discharging_off_callback(self, path, value):
+ return
+
+ def turn_balancing_off_callback(self, path, value):
+ return
diff --git a/etc/dbus-serialbattery/bms/battery_template.py b/etc/dbus-serialbattery/bms/battery_template.py
index e6148e65..e32e424b 100644
--- a/etc/dbus-serialbattery/bms/battery_template.py
+++ b/etc/dbus-serialbattery/bms/battery_template.py
@@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
# NOTES
-# Please also update the feature comparison table, if you are adding a new BMS
-# https://louisvdw.github.io/dbus-serialbattery/general/features/#bms-feature-comparison
+# Please see "Add/Request a new BMS" https://louisvdw.github.io/dbus-serialbattery/general/supported-bms#add-by-opening-a-pull-request
+# in the documentation for a checklist what you have to do, when adding a new BMS
+# avoid importing wildcards
from battery import Protection, Battery, Cell
from utils import is_bit_set, read_serial_data, logger
import utils
@@ -26,7 +27,7 @@ def test_connection(self):
result = False
try:
result = self.read_status_data()
- # get first data to show in startup log
+ # get first data to show in startup log, only if result is true
if result:
self.refresh_data()
except Exception as err:
@@ -104,6 +105,7 @@ def read_serial_data_template(self, command):
command, self.port, self.baud_rate, self.LENGTH_POS, self.LENGTH_CHECK
)
if data is False:
+ logger.error(">>> ERROR: No reply - returning")
return False
start, flag, command_ret, length = unpack_from("BBBB", data)
diff --git a/etc/dbus-serialbattery/bms/daly.py b/etc/dbus-serialbattery/bms/daly.py
index b358a886..87510c48 100644
--- a/etc/dbus-serialbattery/bms/daly.py
+++ b/etc/dbus-serialbattery/bms/daly.py
@@ -24,6 +24,9 @@ def __init__(self, port, baud, address):
self.reset_soc = 0
self.soc_to_set = None
self.runtime = 0 # TROUBLESHOOTING for no reply errors
+ self.trigger_force_disable_discharge = None
+ self.trigger_force_disable_charge = None
+ self.cells_volts_data_lastreadbad = False
# 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"
@@ -40,6 +43,8 @@ def __init__(self, port, baud, address):
command_temp = b"\x96"
command_cell_balance = b"\x97" # no reply
command_alarm = b"\x98" # no reply
+ command_disable_discharge_mos = b"\xD9"
+ command_disable_charge_mos = b"\xDA"
BATTERYTYPE = "Daly"
LENGTH_CHECK = 1
@@ -55,16 +60,19 @@ def test_connection(self):
try:
with open_serial_port(self.port, self.baud_rate) as ser:
result = self.read_status_data(ser)
- self.read_soc_data(ser)
- self.read_battery_code(ser)
- self.reset_soc = (
- self.soc
- ) # set to meaningful value as preset for the GUI
+ # get first data to show in startup log, only if result is true
+ if result:
+ self.read_soc_data(ser)
+ self.read_battery_code(ser)
except Exception as err:
logger.error(f"Unexpected {err=}, {type(err)=}")
result = False
+ # give the user a feedback that no BMS was found
+ if not result:
+ logger.error(">>> ERROR: No reply - returning")
+
return result
def get_settings(self):
@@ -84,32 +92,33 @@ def refresh_data(self):
try:
with open_serial_port(self.port, self.baud_rate) as ser:
result = self.read_soc_data(ser)
+ self.reset_soc = self.soc if self.soc else 0
if self.runtime > 0.200: # TROUBLESHOOTING for no reply errors
logger.info(
" |- refresh_data: read_soc_data - result: "
+ str(result)
+ " - runtime: "
- + str(self.runtime)
+ + str(f"{self.runtime:.1f}")
+ "s"
)
- result = result and self.read_fed_data(ser)
+ result = self.read_fed_data(ser) and result
if self.runtime > 0.200: # TROUBLESHOOTING for no reply errors
logger.info(
" |- refresh_data: read_fed_data - result: "
+ str(result)
+ " - runtime: "
- + str(self.runtime)
+ + str(f"{self.runtime:.1f}")
+ "s"
)
- result = result and self.read_cell_voltage_range_data(ser)
+ result = self.read_cell_voltage_range_data(ser) and result
if self.runtime > 0.200: # TROUBLESHOOTING for no reply errors
logger.info(
" |- refresh_data: read_cell_voltage_range_data - result: "
+ str(result)
+ " - runtime: "
- + str(self.runtime)
+ + str(f"{self.runtime:.1f}")
+ "s"
)
@@ -119,50 +128,52 @@ def refresh_data(self):
" |- refresh_data: write_soc_and_datetime - result: "
+ str(result)
+ " - runtime: "
- + str(self.runtime)
+ + str(f"{self.runtime:.1f}")
+ "s"
)
- result = result and self.read_alarm_data(ser)
+ result = self.read_alarm_data(ser) and result
if self.runtime > 0.200: # TROUBLESHOOTING for no reply errors
logger.info(
" |- refresh_data: read_alarm_data - result: "
+ str(result)
+ " - runtime: "
- + str(self.runtime)
+ + str(f"{self.runtime:.1f}")
+ "s"
)
- result = result and self.read_temperature_range_data(ser)
+ result = self.read_temperature_range_data(ser) and result
if self.runtime > 0.200: # TROUBLESHOOTING for no reply errors
logger.info(
" |- refresh_data: read_temperature_range_data - result: "
+ str(result)
+ " - runtime: "
- + str(self.runtime)
+ + str(f"{self.runtime:.1f}")
+ "s"
)
- result = result and self.read_cells_volts(ser)
+ result = self.read_balance_state(ser) and result
if self.runtime > 0.200: # TROUBLESHOOTING for no reply errors
logger.info(
- " |- refresh_data: read_cells_volts - result: "
+ " |- refresh_data: read_balance_state - result: "
+ str(result)
+ " - runtime: "
- + str(self.runtime)
+ + str(f"{self.runtime:.1f}")
+ "s"
)
- result = result and self.read_balance_state(ser)
+ result = self.read_cells_volts(ser) and result
if self.runtime > 0.200: # TROUBLESHOOTING for no reply errors
logger.info(
- " |- refresh_data: read_balance_state - result: "
+ " |- refresh_data: read_cells_volts - result: "
+ str(result)
+ " - runtime: "
- + str(self.runtime)
+ + str(f"{self.runtime:.1f}")
+ "s"
)
+ self.write_charge_discharge_mos(ser)
+
except OSError:
logger.warning("Couldn't open serial port")
@@ -171,10 +182,10 @@ def refresh_data(self):
return result
def read_status_data(self, ser):
- status_data = self.read_serial_data_daly(ser, self.command_status)
+ status_data = self.request_data(ser, self.command_status)
# check if connection success
if status_data is False:
- logger.warning("No data received in read_status_data()")
+ logger.debug("No data received in read_status_data()")
return False
(
@@ -195,7 +206,7 @@ def read_status_data(self, ser):
+ " cells"
+ (" (" + self.production + ")" if self.production else "")
)
- logger.info(self.hardware_version)
+ logger.debug(self.hardware_version)
return True
def read_soc_data(self, ser):
@@ -205,7 +216,7 @@ def read_soc_data(self, ser):
triesValid = 2
while triesValid > 0:
triesValid -= 1
- soc_data = self.read_serial_data_daly(ser, self.command_soc)
+ soc_data = self.request_data(ser, self.command_soc)
# check if connection success
if soc_data is False:
continue
@@ -226,7 +237,7 @@ def read_soc_data(self, ser):
return False
def read_alarm_data(self, ser):
- alarm_data = self.read_serial_data_daly(ser, self.command_alarm)
+ alarm_data = self.request_data(ser, self.command_alarm)
# check if connection success
if alarm_data is False:
logger.warning("No data received in read_alarm_data()")
@@ -245,57 +256,57 @@ def read_alarm_data(self, ser):
if al_volt & 48:
# High voltage levels - Alarm
- self.voltage_high = 2
+ self.protection.voltage_high = 2
elif al_volt & 15:
# High voltage Warning levels - Pre-alarm
- self.voltage_high = 1
+ self.protection.voltage_high = 1
else:
- self.voltage_high = 0
+ self.protection.voltage_high = 0
if al_volt & 128:
# Low voltage level - Alarm
- self.voltage_low = 2
+ self.protection.voltage_low = 2
elif al_volt & 64:
# Low voltage Warning level - Pre-alarm
- self.voltage_low = 1
+ self.protection.voltage_low = 1
else:
- self.voltage_low = 0
+ self.protection.voltage_low = 0
if al_temp & 2:
# High charge temp - Alarm
- self.temp_high_charge = 2
+ self.protection.temp_high_charge = 2
elif al_temp & 1:
# High charge temp - Pre-alarm
- self.temp_high_charge = 1
+ self.protection.temp_high_charge = 1
else:
- self.temp_high_charge = 0
+ self.protection.temp_high_charge = 0
if al_temp & 8:
# Low charge temp - Alarm
- self.temp_low_charge = 2
+ self.protection.temp_low_charge = 2
elif al_temp & 4:
# Low charge temp - Pre-alarm
- self.temp_low_charge = 1
+ self.protection.temp_low_charge = 1
else:
- self.temp_low_charge = 0
+ self.protection.temp_low_charge = 0
if al_temp & 32:
# High discharge temp - Alarm
- self.temp_high_discharge = 2
+ self.protection.temp_high_discharge = 2
elif al_temp & 16:
# High discharge temp - Pre-alarm
- self.temp_high_discharge = 1
+ self.protection.temp_high_discharge = 1
else:
- self.temp_high_discharge = 0
+ self.protection.temp_high_discharge = 0
if al_temp & 128:
# Low discharge temp - Alarm
- self.temp_low_discharge = 2
+ self.protection.temp_low_discharge = 2
elif al_temp & 64:
# Low discharge temp - Pre-alarm
- self.temp_low_discharge = 1
+ self.protection.temp_low_discharge = 1
else:
- self.temp_low_discharge = 0
+ self.protection.temp_low_discharge = 0
# if al_crnt_soc & 2:
# # High charge current - Alarm
@@ -317,98 +328,90 @@ def read_alarm_data(self, ser):
if al_crnt_soc & 2 or al_crnt_soc & 8:
# High charge/discharge current - Alarm
- self.current_over = 2
+ self.protection.current_over = 2
elif al_crnt_soc & 1 or al_crnt_soc & 4:
# High charge/discharge current - Pre-alarm
- self.current_over = 1
+ self.protection.current_over = 1
else:
- self.current_over = 0
+ self.protection.current_over = 0
if al_crnt_soc & 128:
# Low SoC - Alarm
- self.soc_low = 2
+ self.protection.soc_low = 2
elif al_crnt_soc & 64:
# Low SoC Warning level - Pre-alarm
- self.soc_low = 1
+ self.protection.soc_low = 1
else:
- self.soc_low = 0
+ self.protection.soc_low = 0
return True
def read_cells_volts(self, ser):
- if self.cell_count is not None:
- buffer = bytearray(self.command_base)
- buffer[1] = self.command_address[0] # Always serial 40 or 80
- buffer[2] = self.command_cell_volts[0]
- buffer[12] = sum(buffer[:12]) & 0xFF
-
- # logger.info(f"{bytes(buffer).hex()}")
-
- if (int(self.cell_count) % 3) == 0:
- maxFrame = int(self.cell_count / 3)
- else:
- maxFrame = int(self.cell_count / 3) + 1
- lenFixed = (
- maxFrame * 13
- ) # 0xA5, 0x01, 0x95, 0x08 + 1 byte frame + 6 byte data + 1byte reserved + chksum
-
- cells_volts_data = self.read_serialport_data(
- ser, buffer, self.LENGTH_POS, 0, lenFixed
+ if self.cell_count is None:
+ return True
+
+ # calculate how many sentences we will receive
+ # in each sentence, the bms will send 3 cell voltages
+ # so for a 4s, we will receive 2 sentences
+ if (int(self.cell_count) % 3) == 0:
+ sentences_expected = int(self.cell_count / 3)
+ else:
+ sentences_expected = int(self.cell_count / 3) + 1
+
+ cells_volts_data = self.request_data(
+ ser, self.command_cell_volts, sentences_to_receive=sentences_expected
+ )
+
+ if cells_volts_data is False and self.cells_volts_data_lastreadbad is True:
+ # if this read out and the last one were bad, report error.
+ # (we don't report single errors, as current daly firmware sends corrupted cells volts data occassionally)
+ logger.debug(
+ "No or invalid data has been received repeatedly in read_cells_volts()"
)
- if cells_volts_data is False:
- logger.warning("No data received in read_cells_volts()")
- return False
+ return False
+ elif cells_volts_data is False:
+ # memorize that this read was bad and bail out, ignoring it
+ self.cells_volts_data_lastreadbad = True
+ return True
+ else:
+ # this read was good, so reset error flag
+ self.cells_volts_data_lastreadbad = False
- frameCell = [0, 0, 0]
- lowMin = utils.MIN_CELL_VOLTAGE / 2
- frame = 0
- bufIdx = 0
-
- if len(self.cells) != self.cell_count:
- # init the numbers of cells
- self.cells = []
- for idx in range(self.cell_count):
- self.cells.append(Cell(True))
-
- # logger.warning("data " + bytes(cells_volts_data).hex())
-
- while bufIdx <= len(cells_volts_data) - (
- 4 + 8 + 1
- ): # we at least need 13 bytes to extract the identifiers + 8 bytes payload + checksum
- b1, b2, b3, b4 = unpack_from(">BBBB", cells_volts_data, bufIdx)
- if b1 == 0xA5 and b2 == 0x01 and b3 == 0x95 and b4 == 0x08:
- (
- frame,
- frameCell[0],
- frameCell[1],
- frameCell[2],
- _,
- chk,
- ) = unpack_from(">BhhhBB", cells_volts_data, bufIdx + 4)
- if sum(cells_volts_data[bufIdx : bufIdx + 12]) & 0xFF != chk:
- logger.warning("bad cell voltages checksum")
- else:
- for idx in range(3):
- cellnum = (
- (frame - 1) * 3
- ) + idx # daly is 1 based, driver 0 based
- if cellnum >= self.cell_count:
- break
- cellVoltage = frameCell[idx] / 1000
- self.cells[cellnum].voltage = (
- None if cellVoltage < lowMin else cellVoltage
- )
- bufIdx += 13 # BBBBBhhhBB -> 13 byte
- else:
- bufIdx += 1 # step through buffer to find valid start
- logger.warning("bad cell voltages header")
+ frameCell = [0, 0, 0]
+ lowMin = utils.MIN_CELL_VOLTAGE / 2
+ frame = 0
+
+ if len(self.cells) != self.cell_count:
+ # init the numbers of cells
+ self.cells = []
+ for idx in range(self.cell_count):
+ self.cells.append(Cell(True))
+
+ # logger.warning("data " + bytes(cells_volts_data).hex())
+
+ # from each of the received sentences, read up to 3 voltages
+ for i in range(sentences_expected):
+ (
+ frame,
+ frameCell[0],
+ frameCell[1],
+ frameCell[2],
+ ) = unpack_from(">Bhhh", cells_volts_data, 8 * i)
+ for idx in range(3):
+ cellnum = ((frame - 1) * 3) + idx # daly is 1 based, driver 0 based
+ if cellnum >= self.cell_count:
+ break # ignore possible unused bytes of last sentence
+ cellVoltage = frameCell[idx] / 1000
+ self.cells[cellnum].voltage = (
+ None if cellVoltage < lowMin else cellVoltage
+ )
return True
def read_cell_voltage_range_data(self, ser):
- minmax_data = self.read_serial_data_daly(ser, self.command_minmax_cell_volts)
+ minmax_data = self.request_data(ser, self.command_minmax_cell_volts)
# check if connection success
if minmax_data is False:
- logger.warning("No data received in read_cell_voltage_range_data()")
+ logger.debug("No data received in read_cell_voltage_range_data()")
return False
(
@@ -426,7 +429,7 @@ def read_cell_voltage_range_data(self, ser):
return True
def read_balance_state(self, ser):
- balance_data = self.read_serial_data_daly(ser, self.command_cell_balance)
+ balance_data = self.request_data(ser, self.command_cell_balance)
# check if connection success
if balance_data is False:
logger.debug("No data received in read_balance_state()")
@@ -442,7 +445,7 @@ def read_balance_state(self, ser):
return True
def read_temperature_range_data(self, ser):
- minmax_data = self.read_serial_data_daly(ser, self.command_minmax_temp)
+ minmax_data = self.request_data(ser, self.command_minmax_temp)
# check if connection success
if minmax_data is False:
logger.debug("No data received in read_temperature_range_data()")
@@ -454,7 +457,7 @@ def read_temperature_range_data(self, ser):
return True
def read_fed_data(self, ser):
- fed_data = self.read_serial_data_daly(ser, self.command_fet)
+ fed_data = self.request_data(ser, self.command_fet)
# check if connection success
if fed_data is False:
logger.debug("No data received in read_fed_data()")
@@ -472,10 +475,10 @@ def read_fed_data(self, ser):
# new
def read_capacity(self, ser):
- capa_data = self.read_serial_data_daly(ser, self.command_rated_params)
+ capa_data = self.request_data(ser, self.command_rated_params)
# check if connection success
if capa_data is False:
- logger.warning("No data received in read_capacity()")
+ logger.debug("No data received in read_capacity()")
return False
(capacity, cell_volt) = unpack_from(">LL", capa_data)
@@ -487,10 +490,10 @@ def read_capacity(self, ser):
# new
def read_production_date(self, ser):
- production = self.read_serial_data_daly(ser, self.command_batt_details)
+ production = self.request_data(ser, self.command_batt_details)
# check if connection success
if production is False:
- logger.warning("No data received in read_production_date()")
+ logger.debug("No data received in read_production_date()")
return False
(_, _, year, month, day) = unpack_from(">BBBBB", production)
@@ -499,39 +502,19 @@ def read_production_date(self, ser):
# new
def read_battery_code(self, ser):
- lenFixed = (
- 5 * 13
- ) # batt code field is 35 bytes and we transfer 7 bytes in each telegram
- data = self.read_serialport_data(
- ser,
- self.generate_command(self.command_batt_code),
- self.LENGTH_POS,
- 0,
- lenFixed,
- )
+ data = self.request_data(ser, self.command_batt_code, sentences_to_receive=5)
if data is False:
- logger.warning("No data received in read_battery_code()")
+ logger.debug("No data received in read_battery_code()")
return False
- bufIdx = 0
battery_code = ""
# logger.warning("data " + bytes(cells_volts_data).hex())
- while (
- bufIdx <= len(data) - 13
- ): # we at least need 13 bytes to extract the identifiers + 8 bytes payload + checksum
- b1, b2, b3, b4 = unpack_from(">BBBB", data, bufIdx)
- if b1 == 0xA5 and b2 == 0x01 and b3 == 0x57 and b4 == 0x08:
- _, part, chk = unpack_from(">B7sB", data, bufIdx + 4)
- if sum(data[bufIdx : bufIdx + 12]) & 0xFF != chk:
- logger.warning(
- "bad battery code checksum"
- ) # use string anyhow, just warn
- battery_code += part.decode("utf-8")
- bufIdx += 13 # BBBBB7sB -> 13 byte
- else:
- bufIdx += 1 # step through buffer to find valid start
- logger.warning("bad battery code header")
+ for i in range(5):
+ nr, part = unpack_from(">B7s", data, i * 8)
+ if nr != i + 1:
+ logger.debug("bad battery code index") # use string anyhow, just warn
+ battery_code += part.decode("utf-8")
if battery_code != "":
self.custom_field = sub(
@@ -546,136 +529,6 @@ def read_battery_code(self, ser):
)
return True
- def generate_command(self, command):
- buffer = bytearray(self.command_base)
- buffer[1] = self.command_address[0] # Always serial 40 or 80
- buffer[2] = command[0]
- buffer[12] = sum(buffer[:12]) & 0xFF # checksum calc
- return buffer
-
- def read_serial_data_daly(self, ser, command):
- data = self.read_serialport_data(
- ser, self.generate_command(command), self.LENGTH_POS, self.LENGTH_CHECK
- )
- if data is False:
- logger.info("No reply to cmd " + bytes(command).hex())
- return False
-
- if len(data) <= 12:
- logger.debug("Too short reply to cmd " + bytes(command).hex())
- return False
-
- # search sentence start
- try:
- idx = data.index(0xA5)
- except ValueError:
- logger.debug(
- "No Sentence Start found for reply to cmd " + bytes(command).hex()
- )
- return False
-
- if len(data[idx:]) <= 12:
- logger.debug("Too short reply to cmd " + bytes(command).hex())
- return False
-
- if data[12 + idx] != sum(data[idx : 12 + idx]) & 0xFF:
- logger.debug("Bad checksum in reply to cmd " + bytes(command).hex())
- return False
-
- _, _, _, length = unpack_from(">BBBB", data, idx)
-
- if length == 8:
- return data[4 + idx : length + 4 + idx]
- else:
- logger.debug(
- ">>> ERROR: Incorrect Reply to CMD "
- + bytes(command).hex()
- + ": 0x"
- + bytes(data).hex()
- )
- return False
-
- # Read data from previously openned serial port
- def read_serialport_data(
- self,
- ser,
- command,
- length_pos,
- length_check,
- length_fixed=None,
- length_size=None,
- ):
- try:
- # wait shortly, else the Daly is not ready and throws a lot of no reply errors
- # if you see a lot of errors, try to increase in steps of 0.005
- sleep(0.020)
-
- time_start = time()
- ser.flushOutput()
- ser.flushInput()
- ser.write(command)
-
- length_byte_size = 1
- if length_size is not None:
- if length_size.upper() == "H":
- length_byte_size = 2
- elif length_size.upper() == "I" or length_size.upper() == "L":
- length_byte_size = 4
-
- toread = ser.inWaiting()
-
- while toread < (length_pos + length_byte_size):
- sleep(0.005)
- toread = ser.inWaiting()
- time_run = time() - time_start
- if time_run > 0.500:
- self.runtime = time_run
- logger.error(">>> ERROR: No reply - returning")
- return False
-
- # logger.info('serial data toread ' + str(toread))
- res = ser.read(toread)
- if length_fixed is not None:
- length = length_fixed
- else:
- if len(res) < (length_pos + length_byte_size):
- logger.error(
- ">>> ERROR: No reply - returning [len:" + str(len(res)) + "]"
- )
- return False
- length_size = length_size if length_size is not None else "B"
- length = unpack_from(">" + length_size, res, length_pos)[0]
-
- data = bytearray(res)
-
- packetlen = (
- length_fixed
- if length_fixed is not None
- else length_pos + length_byte_size + length + length_check
- )
- while len(data) < packetlen:
- res = ser.read(packetlen - len(data))
- data.extend(res)
- sleep(0.005)
- time_run = time() - time_start
- if time_run > 0.500:
- self.runtime = time_run
- logger.error(
- ">>> ERROR: No reply - returning [len:"
- + str(len(data))
- + "/"
- + str(length + length_check)
- + "]"
- )
- return False
-
- self.runtime = time_run
- return data
-
- except Exception as e:
- logger.error(e)
- return False
-
def reset_soc_callback(self, path, value):
if value is None:
return False
@@ -715,21 +568,152 @@ def write_soc_and_datetime(self, ser):
logger.info(f"write soc {self.soc_to_set}%")
self.soc_to_set = None # Reset value, so we will set it only once
- time_start = time()
ser.flushOutput()
ser.flushInput()
ser.write(cmd)
+ reply = self.read_sentence(ser, self.command_set_soc)
+ if reply[0] != 1:
+ logger.error("write soc failed")
+ return True
+
+ def force_charging_off_callback(self, path, value):
+ if value is None:
+ return False
+
+ if value == 0:
+ self.trigger_force_disable_charge = False
+ return True
+
+ if value == 1:
+ self.trigger_force_disable_charge = True
+ return True
+
+ return False
+
+ def force_discharging_off_callback(self, path, value):
+ if value is None:
+ return False
+
+ if value == 0:
+ self.trigger_force_disable_discharge = False
+ return True
+
+ if value == 1:
+ self.trigger_force_disable_discharge = True
+ return True
+
+ return False
+
+ def write_charge_discharge_mos(self, ser):
+ if (
+ self.trigger_force_disable_charge is None
+ and self.trigger_force_disable_discharge is None
+ ):
+ return False
+
+ cmd = bytearray(self.command_base)
+
+ if self.trigger_force_disable_charge is not None:
+ cmd[2] = self.command_disable_charge_mos[0]
+ cmd[4] = 0 if self.trigger_force_disable_charge else 1
+ cmd[12] = sum(cmd[:12]) & 0xFF
+ logger.info(
+ f"write force disable charging: {'true' if self.trigger_force_disable_charge else 'false'}"
+ )
+ self.trigger_force_disable_charge = None
+ ser.flushOutput()
+ ser.flushInput()
+ ser.write(cmd)
+
+ reply = self.read_sentence(ser, self.command_disable_charge_mos)
+ if reply is False or reply[0] != cmd[4]:
+ logger.error("write force disable charge/discharge failed")
+ return False
+
+ if self.trigger_force_disable_discharge is not None:
+ cmd[2] = self.command_disable_discharge_mos[0]
+ cmd[4] = 0 if self.trigger_force_disable_discharge else 1
+ cmd[12] = sum(cmd[:12]) & 0xFF
+ logger.info(
+ f"write force disable discharging: {'true' if self.trigger_force_disable_discharge else 'false'}"
+ )
+ self.trigger_force_disable_discharge = None
+ ser.flushOutput()
+ ser.flushInput()
+ ser.write(cmd)
+
+ reply = self.read_sentence(ser, self.command_disable_discharge_mos)
+ if reply is False or reply[0] != cmd[4]:
+ logger.error("write force disable charge/discharge failed")
+ return False
+ return True
+
+ def generate_command(self, command):
+ buffer = bytearray(self.command_base)
+ buffer[1] = self.command_address[0] # Always serial 40 or 80
+ buffer[2] = command[0]
+ buffer[12] = sum(buffer[:12]) & 0xFF # checksum calc
+ return buffer
+
+ def request_data(self, ser, command, sentences_to_receive=1):
+ # wait shortly, else the Daly is not ready and throws a lot of no reply errors
+ # if you see a lot of errors, try to increase in steps of 0.005
+ sleep(0.020)
+
+ self.runtime = 0
+ time_start = time()
+ ser.flushOutput()
+ ser.flushInput()
+ ser.write(self.generate_command(command))
+
+ reply = bytearray()
+ for i in range(sentences_to_receive):
+ next = self.read_sentence(ser, command)
+ if not next:
+ logger.debug(f"request_data: bad reply no. {i}")
+ return False
+ reply += next
+ self.runtime = time() - time_start
+ return reply
+
+ def read_sentence(self, ser, expected_reply, timeout=0.5):
+ """read one 13 byte sentence from daly smart bms.
+ return false if less than 13 bytes received in timeout secs, or frame errors occured
+ return received datasection as bytearray else
+ """
+ time_start = time()
+
+ reply = ser.read_until(b"\xA5")
+ if not reply or b"\xA5" not in reply:
+ logger.debug(
+ f"read_sentence {bytes(expected_reply).hex()}: no sentence start received"
+ )
+ return False
+
+ idx = reply.index(b"\xA5")
+ reply = reply[idx:]
toread = ser.inWaiting()
- while toread < 13:
- sleep(0.005)
+ while toread < 12:
+ sleep((12 - toread) * 0.001)
toread = ser.inWaiting()
time_run = time() - time_start
- if time_run > 0.500:
- logger.warning("write soc: no reply, probably failed")
+ if time_run > timeout:
+ logger.debug(f"read_sentence {bytes(expected_reply).hex()}: timeout")
return False
- reply = ser.read(toread)
- if reply[4] != 1:
- logger.error("write soc failed")
- return True
+ reply += ser.read(12)
+ _, id, cmd, length = unpack_from(">BBBB", reply)
+
+ # logger.info(f"reply: {bytes(reply).hex()}") # debug
+
+ if id != 1 or length != 8 or cmd != expected_reply[0]:
+ logger.debug(f"read_sentence {bytes(expected_reply).hex()}: wrong header")
+ return False
+
+ chk = unpack_from(">B", reply, 12)[0]
+ if sum(reply[:12]) & 0xFF != chk:
+ logger.debug(f"read_sentence {bytes(expected_reply).hex()}: wrong checksum")
+ return False
+
+ return reply[4:12]
diff --git a/etc/dbus-serialbattery/bms/ecs.py b/etc/dbus-serialbattery/bms/ecs.py
index a50e6072..ddfdf901 100644
--- a/etc/dbus-serialbattery/bms/ecs.py
+++ b/etc/dbus-serialbattery/bms/ecs.py
@@ -32,6 +32,7 @@ def test_connection(self):
# Return True if success, False for failure
# Trying to find Green Meter ID
+ result = False
try:
mbdev = minimalmodbus.Instrument(self.port, utils.GREENMETER_ADDRESS)
mbdev.serial.parity = minimalmodbus.serial.PARITY_EVEN
@@ -44,14 +45,28 @@ def test_connection(self):
if tmpId == self.GREENMETER_ID_125A:
self.METER_SIZE = "125A"
+ # TODO
+ # has this to be true?
+ # if yes then self.get_settings() should only be called, if this is true
self.find_LiPro_cells()
- # get first data to show in startup log
- self.refresh_data()
+ result = self.get_settings()
+
+ # get first data to show in startup log, only if result is true
+ if result:
+ self.refresh_data()
- return self.get_settings()
except IOError:
- return False
+ result = False
+ except Exception as err:
+ logger.error(f"Unexpected {err=}, {type(err)=}")
+ result = False
+
+ # give the user a feedback that no BMS was found
+ if not result:
+ logger.error(">>> ERROR: No reply - returning")
+
+ return result
def find_LiPro_cells(self):
# test for LiPro cell devices
diff --git a/etc/dbus-serialbattery/bms/heltecmodbus.py b/etc/dbus-serialbattery/bms/heltecmodbus.py
new file mode 100644
index 00000000..e7f866a0
--- /dev/null
+++ b/etc/dbus-serialbattery/bms/heltecmodbus.py
@@ -0,0 +1,445 @@
+# -*- coding: utf-8 -*-
+# known limitations:
+# - only BMS variants with 2 cell temperature sensors supported
+# - some "interesting" datapoints are not read (e. g. registers 52: switch type, 62: bootloader and firmware version)
+# - SOC not yet resettable from Venus (similary to Daly for support of writing SOC), but modbus write to 120 should be
+# fairly possible)
+
+
+from battery import Battery, Cell
+from utils import logger
+import utils
+import serial
+import time
+import minimalmodbus
+from typing import Dict
+import threading
+
+# the Heltec BMS is not always as responsive as it should, so let's try it up to (RETRYCNT - 1) times to talk to it
+RETRYCNT = 10
+
+# the wait time after a communication - normally this should be as defined by modbus RTU and handled in minimalmodbus,
+# but yeah, it seems we need it for the Heltec BMS
+SLPTIME = 0.03
+
+mbdevs: Dict[int, minimalmodbus.Instrument] = {}
+locks: Dict[int, any] = {}
+
+
+class HeltecModbus(Battery):
+ def __init__(self, port, baud, address):
+ super(HeltecModbus, self).__init__(port, baud, address)
+ self.type = "Heltec_Smart"
+
+ 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
+ for self.address in utils.HELTEC_MODBUS_ADDR:
+ logger.debug("Testing on slave address " + str(self.address))
+ found = False
+ if self.address not in locks:
+ locks[self.address] = threading.Lock()
+
+ # TODO: We need to lock not only based on the address, but based on the port as soon as multiple BMSs
+ # are supported on the same serial interface. Then locking on the port will be enough.
+
+ with locks[self.address]:
+ mbdev = minimalmodbus.Instrument(
+ self.port,
+ slaveaddress=self.address,
+ mode="rtu",
+ close_port_after_each_call=True,
+ debug=False,
+ )
+ mbdev.serial.parity = minimalmodbus.serial.PARITY_NONE
+ mbdev.serial.stopbits = serial.STOPBITS_ONE
+ mbdev.serial.baudrate = 9600
+ # yes, 400ms is long but the BMS is sometimes really slow in responding, so this is a good compromise
+ mbdev.serial.timeout = 0.4
+ mbdevs[self.address] = mbdev
+
+ for n in range(1, RETRYCNT):
+ try:
+ string = mbdev.read_string(7, 13)
+ time.sleep(SLPTIME)
+ found = True
+ logger.debug(
+ "found in try "
+ + str(n)
+ + "/"
+ + str(RETRYCNT)
+ + " for "
+ + self.port
+ + "("
+ + str(self.address)
+ + "): "
+ + string
+ )
+ except Exception as e:
+ logger.debug(
+ "testing failed ("
+ + str(e)
+ + ") "
+ + str(n)
+ + "/"
+ + str(RETRYCNT)
+ + " for "
+ + self.port
+ + "("
+ + str(self.address)
+ + ")"
+ )
+ continue
+ break
+ if found:
+ self.type = "#" + str(self.address) + "_Heltec_Smart"
+ break
+
+ # give the user a feedback that no BMS was found
+ if not found:
+ logger.error(">>> ERROR: No reply - returning")
+
+ return (
+ found
+ and self.read_status_data()
+ and self.get_settings()
+ and self.refresh_data()
+ )
+
+ def get_settings(self):
+ self.max_battery_voltage = self.max_cell_voltage * self.cell_count
+ self.min_battery_voltage = self.min_cell_voltage * self.cell_count
+
+ return True
+
+ def refresh_data(self):
+ # call all functions that will refresh the battery data.
+ # This will be called for every iteration (1 second)
+ # Return True if success, False for failure
+ return self.read_soc_data() and self.read_cell_data()
+
+ def read_status_data(self):
+ mbdev = mbdevs[self.address]
+
+ with locks[self.address]:
+ for n in range(1, RETRYCNT + 1):
+ try:
+ ccur = mbdev.read_register(191, 0, 3, False)
+ self.max_battery_charge_current = (
+ (int)(((ccur & 0xFF) << 8) | ((ccur >> 8) & 0xFF))
+ ) / 100
+ time.sleep(SLPTIME)
+
+ dc = mbdev.read_register(194, 0, 3, False)
+ self.max_battery_discharge_current = (
+ ((dc & 0xFF) << 8) | ((dc >> 8) & 0xFF)
+ ) / 100
+ time.sleep(SLPTIME)
+
+ cap = mbdev.read_register(118, 0, 3, False)
+ self.capacity = (((cap & 0xFF) << 8) | ((cap >> 8) & 0xFF)) / 10
+ time.sleep(SLPTIME)
+
+ cap = mbdev.read_register(119, 0, 3, False)
+ self.actual_capacity = (
+ ((cap & 0xFF) << 8) | ((cap >> 8) & 0xFF)
+ ) / 10
+ time.sleep(SLPTIME)
+
+ cap = mbdev.read_register(126, 0, 3, False)
+ self.learned_capacity = (
+ ((cap & 0xFF) << 8) | ((cap >> 8) & 0xFF)
+ ) / 10
+ time.sleep(SLPTIME)
+
+ volt = mbdev.read_register(169, 0, 3, False)
+ self.max_cell_voltage = (
+ ((volt & 0xFF) << 8) | ((volt >> 8) & 0xFF)
+ ) / 1000
+ time.sleep(SLPTIME)
+
+ volt = mbdev.read_register(172, 0, 3, False)
+ self.min_cell_voltage = (
+ ((volt & 0xFF) << 8) | ((volt >> 8) & 0xFF)
+ ) / 1000
+ time.sleep(SLPTIME)
+
+ string = mbdev.read_string(7, 13)
+ self.hwTypeName = string
+ time.sleep(SLPTIME)
+
+ string = mbdev.read_string(41, 6)
+ self.devName = string
+ time.sleep(SLPTIME)
+
+ serial1 = mbdev.read_registers(2, number_of_registers=4)
+ self.unique_identifier = "-".join(
+ "{:04x}".format(x) for x in serial1
+ )
+ time.sleep(SLPTIME)
+
+ self.pw = mbdev.read_string(47, 2)
+ time.sleep(SLPTIME)
+
+ tmp = mbdev.read_register(75)
+ # h: batterytype: 0: Ternery Lithium, 1: Iron Lithium, 2: Lithium Titanat
+ # l: #of cells
+
+ self.cell_count = (tmp >> 8) & 0xFF
+ tmp = tmp & 0xFF
+ if tmp == 0:
+ self.cellType = "Ternary Lithium"
+ elif tmp == 1:
+ self.cellType = "Iron Lithium"
+ elif tmp == 2:
+ self.cellType = "Lithium Titatnate"
+ else:
+ self.cellType = "unknown"
+ time.sleep(SLPTIME)
+
+ self.hardware_version = (
+ self.devName
+ + "("
+ + str((mbdev.read_register(38) >> 8) & 0xFF)
+ + ")"
+ )
+ time.sleep(SLPTIME)
+
+ date = mbdev.read_long(39, 3, True, minimalmodbus.BYTEORDER_LITTLE)
+ self.production_date = (
+ str(date & 0xFFFF)
+ + "-"
+ + str((date >> 24) & 0xFF)
+ + "-"
+ + str((date >> 16) & 0xFF)
+ )
+ time.sleep(SLPTIME)
+
+ # we finished all readings without trouble, so let's break from the retry loop
+ break
+ except Exception as e:
+ logger.warn(
+ "Error reading settings from BMS, retry ("
+ + str(n)
+ + "/"
+ + str(RETRYCNT)
+ + "): "
+ + str(e)
+ )
+ if n == RETRYCNT:
+ return False
+ continue
+
+ logger.info(self.hardware_version)
+ logger.info("Heltec-" + self.hwTypeName)
+ logger.info(" Dev name: " + self.devName)
+ logger.info(" Serial: " + self.unique_identifier)
+ logger.info(" Made on: " + self.production_date)
+ logger.info(" Cell count: " + str(self.cell_count))
+ logger.info(" Cell type: " + self.cellType)
+ logger.info(" BT password: " + self.pw)
+ logger.info(" rated capacity: " + str(self.capacity))
+ logger.info(" actual capacity: " + str(self.actual_capacity))
+ logger.info(" learned capacity: " + str(self.learned_capacity))
+
+ return True
+
+ def read_soc_data(self):
+ mbdev = mbdevs[self.address]
+
+ with locks[self.address]:
+ for n in range(1, RETRYCNT):
+ try:
+ self.voltage = (
+ mbdev.read_long(76, 3, True, minimalmodbus.BYTEORDER_LITTLE)
+ / 1000
+ )
+ time.sleep(SLPTIME)
+
+ self.current = -(
+ mbdev.read_long(78, 3, True, minimalmodbus.BYTEORDER_LITTLE)
+ / 100
+ )
+ time.sleep(SLPTIME)
+
+ runState1 = mbdev.read_long(
+ 152, 3, True, minimalmodbus.BYTEORDER_LITTLE
+ )
+ time.sleep(SLPTIME)
+
+ # bit 29 is discharge protection
+ if (runState1 & 0x20000000) == 0:
+ self.discharge_fet = True
+ else:
+ self.discharge_fet = False
+
+ # bit 28 is charge protection
+ if (runState1 & 0x10000000) == 0:
+ self.charge_fet = True
+ else:
+ self.charge_fet = False
+
+ warnings = mbdev.read_long(
+ 156, 3, True, minimalmodbus.BYTEORDER_LITTLE
+ )
+ if (warnings & (1 << 3)) or (
+ warnings & (1 << 15)
+ ): # 15 is full protection, 3 is total overvoltage
+ self.voltage_high = 2
+ else:
+ self.voltage_high = 0
+
+ if warnings & (1 << 0):
+ self.protection.voltage_cell_high = 2
+ # we handle a single cell OV as total OV, as long as cell_high is not explicitly handled
+ self.protection.voltage_high = 1
+ else:
+ self.protection.voltage_cell_high = 0
+
+ if warnings & (1 << 1):
+ self.protection.voltage_cell_low = 2
+ else:
+ self.protection.voltage_cell_low = 0
+
+ if warnings & (1 << 4):
+ self.protection.voltage_low = 2
+ else:
+ self.protection.voltage_low = 0
+
+ if warnings & (1 << 5):
+ self.protection.current_over = 2
+ else:
+ self.protection.current_over = 0
+
+ if warnings & (1 << 7):
+ self.protection.current_under = 2
+ elif warnings & (1 << 6):
+ self.protection.current_under = 1
+ else:
+ self.protection.current_under = 0
+
+ if warnings & (1 << 8): # this is a short circuit
+ self.protection.current_over = 2
+
+ if warnings & (1 << 9):
+ self.protection.temp_high_charge = 2
+ else:
+ self.protection.temp_high_charge = 0
+
+ if warnings & (1 << 10):
+ self.protection.temp_low_charge = 2
+ else:
+ self.protection.temp_low_charge = 0
+
+ if warnings & (1 << 11):
+ self.protection.temp_high_discharge = 2
+ else:
+ self.protection.temp_high_discharge = 0
+
+ if warnings & (1 << 12):
+ self.protection.temp_low_discharge = 2
+ else:
+ self.protection.temp_low_discharge = 0
+
+ if warnings & (1 << 13): # MOS overtemp
+ self.protection.temp_high_internal = 2
+ else:
+ self.protection.temp_high_internal = 0
+
+ if warnings & (1 << 14): # SOC low
+ self.protection.soc_low = 2
+ else:
+ self.protection.soc_low = 0
+
+ if warnings & (0xFFFF0000): # any other fault
+ self.protection.internal_failure = 2
+ else:
+ self.protection.internal_failure = 0
+
+ socsoh = mbdev.read_register(120, 0, 3, False)
+ self.soh = socsoh & 0xFF
+ self.soc = (socsoh >> 8) & 0xFF
+ time.sleep(SLPTIME)
+
+ # we could read min and max temperature, here, but I have a BMS with only 2 sensors,
+ # so I couldn't test the logic and read therefore only the first two temperatures
+ # tminmax = mbdev.read_register(117, 0, 3, False)
+ # nmin = (tminmax & 0xFF)
+ # nmax = ((tminmax >> 8) & 0xFF)
+
+ temps = mbdev.read_register(113, 0, 3, False)
+ self.temp1 = (temps & 0xFF) - 40
+ self.temp2 = ((temps >> 8) & 0xFF) - 40
+ time.sleep(SLPTIME)
+
+ temps = mbdev.read_register(112, 0, 3, False)
+ most = (temps & 0xFF) - 40
+ balt = ((temps >> 8) & 0xFF) - 40
+ # balancer temperature is not handled separately in dbus-serialbattery,
+ # so let's display the max of both temperatures inside the BMS as mos temperature
+ self.temp_mos = max(most, balt)
+ time.sleep(SLPTIME)
+
+ return True
+
+ except Exception as e:
+ logger.warn(
+ "Error reading SOC, retry ("
+ + str(n)
+ + "/"
+ + str(RETRYCNT)
+ + ") "
+ + str(e)
+ )
+ continue
+ break
+ logger.warn("Error reading SOC, failed")
+ return False
+
+ def read_cell_data(self):
+ result = False
+ mbdev = mbdevs[self.address]
+
+ with locks[self.address]:
+ for n in range(1, RETRYCNT):
+ try:
+ cells = mbdev.read_registers(
+ 81, number_of_registers=self.cell_count
+ )
+ time.sleep(SLPTIME)
+
+ balancing = mbdev.read_long(
+ 139, 3, signed=False, byteorder=minimalmodbus.BYTEORDER_LITTLE
+ )
+ time.sleep(SLPTIME)
+
+ result = True
+ except Exception as e:
+ logger.warn(
+ "read_cell_data() failed ("
+ + str(e)
+ + ") "
+ + str(n)
+ + "/"
+ + str(RETRYCNT)
+ )
+ continue
+ break
+ if result is False:
+ return False
+
+ if len(self.cells) != self.cell_count:
+ self.cells = []
+ for idx in range(self.cell_count):
+ self.cells.append(Cell(False))
+
+ i = 0
+ for cell in cells:
+ cellV = ((cell & 0xFF) << 8) | ((cell >> 8) & 0xFF)
+ self.cells[i].voltage = cellV / 1000
+ self.cells[i].balance = balancing & (1 << i) != 0
+
+ i = i + 1
+
+ return True
diff --git a/etc/dbus-serialbattery/bms/hlpdatabms4s.py b/etc/dbus-serialbattery/bms/hlpdatabms4s.py
index d4a350b1..7faf8b2c 100644
--- a/etc/dbus-serialbattery/bms/hlpdatabms4s.py
+++ b/etc/dbus-serialbattery/bms/hlpdatabms4s.py
@@ -24,6 +24,10 @@ def test_connection(self):
logger.error(f"Unexpected {err=}, {type(err)=}")
result = False
+ # give the user a feedback that no BMS was found
+ if not result:
+ logger.error(">>> ERROR: No reply - returning")
+
return result
def get_settings(self):
@@ -210,6 +214,9 @@ def read_serial_data2(command, port, baud, time, min_len):
logger.error(e)
return False
+ except Exception:
+ return False
+
def read_serialport_data2(ser, command, time, min_len):
try:
diff --git a/etc/dbus-serialbattery/bms/jkbms_ble.py b/etc/dbus-serialbattery/bms/jkbms_ble.py
index 03a6e47e..276103c4 100644
--- a/etc/dbus-serialbattery/bms/jkbms_ble.py
+++ b/etc/dbus-serialbattery/bms/jkbms_ble.py
@@ -14,6 +14,7 @@ class Jkbms_Ble(Battery):
def __init__(self, port, baud, address):
super(Jkbms_Ble, self).__init__(address.replace(":", "").lower(), baud, address)
+ self.address = address
self.type = self.BATTERYTYPE
self.jk = Jkbms_Brn(address)
diff --git a/etc/dbus-serialbattery/bms/lifepower.py b/etc/dbus-serialbattery/bms/lifepower.py
index 84c93a37..b46421fb 100644
--- a/etc/dbus-serialbattery/bms/lifepower.py
+++ b/etc/dbus-serialbattery/bms/lifepower.py
@@ -38,7 +38,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 = utils.MAX_BATTERY_CURRENT
+ self.max_battery_charge_current = utils.MAX_BATTERY_CHARGE_CURRENT
self.max_battery_discharge_current = utils.MAX_BATTERY_DISCHARGE_CURRENT
hardware_version = self.read_serial_data_eg4(self.command_hardware_version)
if hardware_version:
@@ -159,7 +159,7 @@ def read_serial_data_eg4(self, command):
self.LENGTH_FIXED,
)
if data is False:
- logger.error(">>> ERROR: Incorrect Data")
+ logger.debug(">>> ERROR: Incorrect Data")
return False
# 0x0D always terminates the response
diff --git a/etc/dbus-serialbattery/bms/lltjbd.py b/etc/dbus-serialbattery/bms/lltjbd.py
index a74697cc..63de584b 100644
--- a/etc/dbus-serialbattery/bms/lltjbd.py
+++ b/etc/dbus-serialbattery/bms/lltjbd.py
@@ -5,6 +5,166 @@
from struct import unpack_from
import struct
+# Protocol registers
+REG_ENTER_FACTORY = 0x00
+REG_EXIT_FACTORY = 0x01
+# REG_UNKNOWN = 0x02
+REG_GENERAL = 0x03
+REG_CELL = 0x04
+REG_HARDWARE = 0x05
+# Firmware 0x16+
+REG_USE_PASSWORD = 0x06
+REG_SET_PASSWORD = 0x07
+# REG_UNKNOWN2 = 0x08 - Maybe define master password?
+REG_CLEAR_PASSWORD = 0x09
+
+REG_FRESET = 0x0A
+
+REG_DESIGN_CAP = 0x10
+REG_CYCLE_CAP = 0x11
+REG_CAP_100 = 0x12
+REG_CAP_0 = 0x13
+REG_SELF_DSG_RATE = 0x14
+REG_MFG_DATE = 0x15
+REG_SERIAL_NUM = 0x16
+REG_CYCLE_CNT = 0x17
+REG_CHGOT = 0x18
+REG_CHGOT_REL = 0x19
+REG_CHGUT = 0x1A
+REG_CHGUT_REL = 0x1B
+REG_DSGOT = 0x1C
+REG_DSGOT_REL = 0x1D
+REG_DSGUT = 0x1E
+REG_DSGUT_REL = 0x1F
+REG_POVP = 0x20
+REG_POVP_REL = 0x21
+REG_PUVP = 0x22
+REG_PUVP_REL = 0x23
+REG_COVP = 0x24
+REG_COVP_REL = 0x25
+REG_CUVP = 0x26
+REG_CUVP_REL = 0x27
+REG_CHGOC = 0x28
+REG_DSGOC = 0x29
+REG_BAL_START = 0x2A
+REG_BAL_WINDOW = 0x2B
+REG_SHUNT_RES = 0x2C
+REG_FUNC_CONFIG = 0x2D
+REG_NTC_CONFIG = 0x2E
+REG_CELL_CNT = 0x2F
+REG_FET_TIME = 0x30
+REG_LED_TIME = 0x31
+REG_CAP_80 = 0x32
+REG_CAP_60 = 0x33
+REG_CAP_40 = 0x34
+REG_CAP_20 = 0x35
+REG_COVP_HIGH = 0x36
+REG_CUVP_HIGH = 0x37
+REG_SC_DSGOC2 = 0x38
+REG_CXVP_HIGH_DELAY_SC_REL = 0x39
+REG_CHG_T_DELAYS = 0x3A
+REG_DSG_T_DELAYS = 0x3B
+REG_PACK_V_DELAYS = 0x3C
+REG_CELL_V_DELAYS = 0x3D
+REG_CHGOC_DELAYS = 0x3E
+REG_DSGOC_DELAYS = 0x3F
+REG_GPSOFF = 0x40
+REG_GPSOFF_TIME = 0x41
+REG_CAP_90 = 0x42
+REG_CAP_70 = 0x43
+REG_CAP_50 = 0x44
+REG_CAP_30 = 0x45
+REG_CAP_10 = 0x46
+# REG_CAP2_100 = 0x47
+
+# [0x48, 0x9F] - 87 registers
+
+REG_MFGNAME = 0xA0
+REG_MODEL = 0xA1
+REG_BARCODE = 0xA2
+REG_ERROR = 0xAA
+# 0xAB
+# 0xAC
+REG_CAL_CUR_IDLE = 0xAD
+REG_CAL_CUR_CHG = 0xAE
+REG_CAL_CUR_DSG = 0xAF
+
+REG_CAL_V_CELL_01 = 0xB0
+REG_CAL_V_CELL_02 = 0xB1
+REG_CAL_V_CELL_03 = 0xB2
+REG_CAL_V_CELL_04 = 0xB3
+REG_CAL_V_CELL_05 = 0xB4
+REG_CAL_V_CELL_06 = 0xB5
+REG_CAL_V_CELL_07 = 0xB6
+REG_CAL_V_CELL_08 = 0xB7
+REG_CAL_V_CELL_09 = 0xB8
+REG_CAL_V_CELL_10 = 0xB9
+REG_CAL_V_CELL_11 = 0xBA
+REG_CAL_V_CELL_12 = 0xBB
+REG_CAL_V_CELL_13 = 0xBC
+REG_CAL_V_CELL_14 = 0xBD
+REG_CAL_V_CELL_15 = 0xBE
+REG_CAL_V_CELL_16 = 0xBF
+REG_CAL_V_CELL_17 = 0xC0
+REG_CAL_V_CELL_18 = 0xC1
+REG_CAL_V_CELL_19 = 0xC2
+REG_CAL_V_CELL_20 = 0xC3
+REG_CAL_V_CELL_21 = 0xC4
+REG_CAL_V_CELL_22 = 0xC5
+REG_CAL_V_CELL_23 = 0xC6
+REG_CAL_V_CELL_24 = 0xC7
+REG_CAL_V_CELL_25 = 0xC8
+REG_CAL_V_CELL_26 = 0xC9
+REG_CAL_V_CELL_27 = 0xCA
+REG_CAL_V_CELL_28 = 0xCB
+REG_CAL_V_CELL_29 = 0xCC
+REG_CAL_V_CELL_30 = 0xCD
+REG_CAL_V_CELL_31 = 0xCE
+REG_CAL_V_CELL_32 = 0xCF
+
+REG_CAL_T_NTC_0 = 0xD0
+REG_CAL_T_NTC_1 = 0xD1
+REG_CAL_T_NTC_2 = 0xD2
+REG_CAL_T_NTC_3 = 0xD3
+REG_CAL_T_NTC_4 = 0xD4
+REG_CAL_T_NTC_5 = 0xD5
+REG_CAL_T_NTC_6 = 0xD6
+REG_CAL_T_NTC_7 = 0xD7
+
+REG_CAP_REMAINING = 0xE0
+REG_CTRL_MOSFET = 0xE1
+REG_CTRL_BALANCE = 0xE2
+REG_RESET = 0xE3
+
+# Protocol commands
+CMD_ENTER_FACTORY_MODE = b"\x56\x78"
+CMD_EXIT_FACTORY_MODE = b"\x00\x00"
+CMD_EXIT_AND_SAVE_FACTORY_MODE = b"\x28\x28"
+
+
+def checksum(payload):
+ return (0x10000 - sum(payload)) % 0x10000
+
+
+def cmd(op, reg, data):
+ payload = [reg, len(data)] + list(data)
+ chksum = checksum(payload)
+ data = [0xDD, op] + payload + [chksum, 0x77]
+ format = f">BB{len(payload)}BHB"
+ return struct.pack(format, *data)
+
+
+def readCmd(reg, data=None):
+ if data is None:
+ data = []
+ return cmd(0xA5, reg, data)
+
+
+def writeCmd(reg, data=None):
+ if data is None:
+ data = []
+ return cmd(0x5A, reg, data)
+
class LltJbdProtection(Protection):
def __init__(self):
@@ -51,24 +211,32 @@ def __init__(self, port, baud, address):
super(LltJbd, self).__init__(port, baud, address)
self.protection = LltJbdProtection()
self.type = self.BATTERYTYPE
+ self._product_name: str = ""
+ self.has_settings = 0
+ self.reset_soc = 100
+ self.soc_to_set = None
+ self.factory_mode = False
+ self.writable = False
# degree_sign = u'\N{DEGREE SIGN}'
- command_general = b"\xDD\xA5\x03\x00\xFF\xFD\x77"
- command_cell = b"\xDD\xA5\x04\x00\xFF\xFC\x77"
- command_hardware = b"\xDD\xA5\x05\x00\xFF\xFB\x77"
BATTERYTYPE = "LLT/JBD"
LENGTH_CHECK = 6
LENGTH_POS = 3
+ command_general = readCmd(REG_GENERAL) # b"\xDD\xA5\x03\x00\xFF\xFD\x77"
+ command_cell = readCmd(REG_CELL) # b"\xDD\xA5\x04\x00\xFF\xFC\x77"
+ command_hardware = readCmd(REG_HARDWARE) # b"\xDD\xA5\x05\x00\xFF\xFB\x77"
+
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_hardware_data()
- # get first data to show in startup log
+ result = self.get_settings()
+ # get first data to show in startup log, only if result is true
if result:
+ self.read_hardware_data()
self.refresh_data()
except Exception as err:
logger.error(f"Unexpected {err=}, {type(err)=}")
@@ -76,12 +244,50 @@ def test_connection(self):
return result
+ def product_name(self) -> str:
+ return self._product_name
+
def get_settings(self):
- self.read_gen_data()
+ if not self.read_gen_data():
+ return False
self.max_battery_charge_current = utils.MAX_BATTERY_CHARGE_CURRENT
self.max_battery_discharge_current = utils.MAX_BATTERY_DISCHARGE_CURRENT
+ with self.eeprom(writable=False):
+ 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
+ )
+ 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
+ )
+
return True
+ def reset_soc_callback(self, path, value):
+ if value is None:
+ return False
+
+ if value < 0 or value > 100:
+ return False
+
+ self.reset_soc = value
+ self.soc_to_set = value
+ return True
+
+ def write_soc(self):
+ if self.soc_to_set is None or self.soc_to_set != 100 or not self.voltage:
+ return False
+ logger.info(f"write soc {self.soc_to_set}%")
+ self.soc_to_set = None # Reset value, so we will set it only once
+ # TODO implement logic to map current pack readings into
+ # REG_CAP_100, REG_CAP_90, REG_CAP_80, REG_CAP_70, REG_CAP_60, ...
+ with self.eeprom(writable=True):
+ pack_voltage = struct.pack(">H", int(self.voltage * 10))
+ self.read_serial_data_llt(writeCmd(REG_CAP_100, pack_voltage))
+
def refresh_data(self):
result = self.read_gen_data()
result = result and self.read_cell_data()
@@ -116,6 +322,43 @@ def to_protection_bits(self, byte_data):
self.protection.set_short = is_bit_set(tmp[2])
def to_cell_bits(self, byte_data, byte_data_high):
+ # init the cell array once
+ if len(self.cells) == 0:
+ for _ in range(self.cell_count):
+ print("#" + str(_))
+ self.cells.append(Cell(False))
+
+ # get up to the first 16 cells
+ tmp = bin(byte_data)[2:].rjust(min(self.cell_count, 16), utils.zero_char)
+ # 4 cells
+ # tmp = 0101
+ # 16 cells
+ # tmp = 0101010101010101
+
+ tmp_reversed = list(reversed(tmp))
+ # print(tmp_reversed) --> ['1', '0', '1', '0', '1', '0', '1', '0', '1', '0', '1', '0', '1', '0', '1', '0']
+ # [cell1, cell2, cell3, ...]
+
+ if self.cell_count > 16:
+ tmp2 = bin(byte_data_high)[2:].rjust(self.cell_count - 16, utils.zero_char)
+ # tmp = 1100110011001100
+ tmp_reversed = tmp_reversed + list(reversed(tmp2))
+ # print(tmp_reversed) --> [
+ # '1', '0', '1', '0', '1', '0', '1', '0', '1', '0', '1', '0', '1', '0', '1', '0',
+ # '0', '0', '1', '1', '0', '0', '1', '1', '0', '0', '1', '1', '0', '0', '1', '1'
+ # ]
+ # [
+ # cell1, cell2, ..., cell16,
+ # cell17, cell18, ..., cell32
+ # ]
+
+ for c in range(self.cell_count):
+ if is_bit_set(tmp_reversed[c]):
+ self.cells[c].balance = True
+ else:
+ self.cells[c].balance = False
+
+ """
# clear the list
for c in self.cells:
self.cells.remove(c)
@@ -128,6 +371,7 @@ def to_cell_bits(self, byte_data, byte_data_high):
tmp = bin(byte_data_high)[2:].rjust(self.cell_count - 16, utils.zero_char)
for bit in reversed(tmp):
self.cells.append(Cell(is_bit_set(bit)))
+ """
def to_fet_bits(self, byte_data):
tmp = bin(byte_data)[2:].rjust(2, utils.zero_char)
@@ -162,7 +406,9 @@ def read_gen_data(self):
self.capacity_remain = capacity_remain / 100
self.capacity = capacity / 100
self.to_cell_bits(balance, balance2)
- self.version = float(str(version >> 4 & 0x0F) + "." + str(version & 0x0F))
+ self.hardware_version = float(
+ str(version >> 4 & 0x0F) + "." + str(version & 0x0F)
+ )
self.to_fet_bits(fet)
self.to_protection_bits(protection)
self.max_battery_voltage = utils.MAX_CELL_VOLTAGE * self.cell_count
@@ -196,24 +442,73 @@ def read_hardware_data(self):
if hardware_data is False:
return False
- self.hardware_version = unpack_from(
+ self._product_name = unpack_from(
">" + str(len(hardware_data)) + "s", hardware_data
- )[0].decode()
- logger.debug(self.hardware_version)
+ )[0].decode("ascii", errors="ignore")
+ logger.debug(self._product_name)
return True
+ @staticmethod
+ def validate_packet(data):
+ if not data:
+ return False
+
+ if data is False:
+ return False
+
+ start, op, status, payload_length = unpack_from("BBBB", data)
+ if start != 0xDD:
+ logger.error(
+ ">>> ERROR: Invalid response packet. Expected begin packet character 0xDD"
+ )
+ if status != 0x0:
+ logger.warn(">>> WARN: BMS rejected request. Status " + status)
+ return False
+ if len(data) != payload_length + 7:
+ logger.error(
+ ">>> ERROR: BMS send insufficient data. Received "
+ + str(len(data))
+ + " expected "
+ + str(payload_length + 7)
+ )
+ return False
+ chk_sum, end = unpack_from(">HB", data, payload_length + 4)
+ if end != 0x77:
+ logger.error(
+ ">>> ERROR: Incorrect Reply. Expected end packet character 0x77"
+ )
+ return False
+ if chk_sum != checksum(data[2:-3]):
+ logger.error(">>> ERROR: Invalid checksum.")
+ return False
+
+ payload = data[4 : payload_length + 4]
+
+ return payload
+
def read_serial_data_llt(self, command):
data = read_serial_data(
command, self.port, self.baud_rate, self.LENGTH_POS, self.LENGTH_CHECK
)
- if data is False:
- return False
+ return self.validate_packet(data)
- start, flag, command_ret, length = unpack_from("BBBB", data)
- checksum, end = unpack_from("HB", data, length + 4)
+ def __enter__(self):
+ if self.read_serial_data_llt(
+ writeCmd(REG_ENTER_FACTORY, CMD_ENTER_FACTORY_MODE)
+ ):
+ self.factory_mode = True
- if end == 119:
- return data[4 : length + 4]
- else:
- logger.error(">>> ERROR: Incorrect Reply")
- return False
+ def __exit__(self, type, value, traceback):
+ cmd_value = (
+ CMD_EXIT_AND_SAVE_FACTORY_MODE if self.writable else CMD_EXIT_FACTORY_MODE
+ )
+ if self.factory_mode:
+ if not self.read_serial_data_llt(writeCmd(REG_EXIT_FACTORY, cmd_value)):
+ logger.error(">>> ERROR: Unable to exit factory mode.")
+ else:
+ self.factory_mode = False
+ self.writable = False
+
+ def eeprom(self, writable=False):
+ self.writable = writable
+ return self
diff --git a/etc/dbus-serialbattery/bms/lltjbd_ble.py b/etc/dbus-serialbattery/bms/lltjbd_ble.py
index fa4b38da..de995492 100644
--- a/etc/dbus-serialbattery/bms/lltjbd_ble.py
+++ b/etc/dbus-serialbattery/bms/lltjbd_ble.py
@@ -3,13 +3,12 @@
import atexit
import functools
import threading
+from asyncio import CancelledError
from typing import Union, Optional
from utils import logger
-from struct import unpack_from
from bleak import BleakClient, BleakScanner, BLEDevice
from bms.lltjbd import LltJbdProtection, LltJbd
-
BLE_SERVICE_UUID = "0000ff00-0000-1000-8000-00805f9b34fb"
BLE_CHARACTERISTICS_TX_UUID = "0000ff02-0000-1000-8000-00805f9b34fb"
BLE_CHARACTERISTICS_RX_UUID = "0000ff01-0000-1000-8000-00805f9b34fb"
@@ -21,7 +20,9 @@ class LltJbd_Ble(LltJbd):
BATTERYTYPE = "LltJbd_Ble"
def __init__(self, port: Optional[str], baud: Optional[int], address: str):
- super(LltJbd_Ble, self).__init__(address.replace(":", "").lower(), -1, address)
+ super(LltJbd_Ble, self).__init__(
+ "ble" + address.replace(":", "").lower(), -1, address
+ )
self.address = address
self.protection = LltJbdProtection()
@@ -50,9 +51,14 @@ def on_disconnect(self, client):
logger.info("BLE client disconnected")
async def bt_main_loop(self):
- self.device = await BleakScanner.find_device_by_address(
- self.address, cb=dict(use_bdaddr=True)
- )
+ try:
+ self.device = await BleakScanner.find_device_by_address(
+ self.address, cb=dict(use_bdaddr=True)
+ )
+ except Exception as e:
+ logger.error(">>> ERROR: Bluetooth stack failed.", e)
+ self.device = None
+ await asyncio.sleep(0.5)
if not self.device:
self.run = False
@@ -155,22 +161,18 @@ async def async_read_serial_data_llt(self, command):
def read_serial_data_llt(self, command):
if not self.bt_loop:
return False
- data = asyncio.run(self.async_read_serial_data_llt(command))
- if not data:
+ try:
+ data = asyncio.run(self.async_read_serial_data_llt(command))
+ return self.validate_packet(data)
+ except CancelledError as e:
+ logger.error(">>> ERROR: No reply - canceled - returning", e)
return False
-
- start, flag, command_ret, length = unpack_from("BBBB", data)
- checksum, end = unpack_from("HB", data, length + 4)
-
- if end == 119:
- return data[4 : length + 4]
- else:
- logger.error(">>> ERROR: Incorrect Reply")
+ except Exception as e:
+ logger.error(">>> ERROR: No reply - returning", e)
return False
-"""
-async def test_LltJbd_Ble():
+if __name__ == "__main__":
import sys
bat = LltJbd_Ble("Foo", -1, sys.argv[1])
@@ -178,8 +180,4 @@ async def test_LltJbd_Ble():
logger.error(">>> ERROR: Unable to connect")
else:
bat.refresh_data()
-
-
-if __name__ == "__main__":
- test_LltJbd_Ble()
-"""
+ bat.get_settings()
diff --git a/etc/dbus-serialbattery/bms/mnb.py b/etc/dbus-serialbattery/bms/mnb.py
index 1ad1126e..84365866 100644
--- a/etc/dbus-serialbattery/bms/mnb.py
+++ b/etc/dbus-serialbattery/bms/mnb.py
@@ -169,9 +169,9 @@ def manage_charge_current(self):
# Change depending on the cell_min_voltage values
if self.cell_min_voltage < self.V_C_min + 0.05:
- self.control_allow_dicharge = False
+ self.control_allow_discharge = False
else:
- self.control_allow_dicharge = True
+ self.control_allow_discharge = True
if self.cell_min_voltage < self.V_C_min + 0.15:
b = 10 * (self.cell_min_voltage - self.V_C_min - 0.05)
diff --git a/etc/dbus-serialbattery/bms/seplos.py b/etc/dbus-serialbattery/bms/seplos.py
index 083d6bc4..b7c9a2e1 100644
--- a/etc/dbus-serialbattery/bms/seplos.py
+++ b/etc/dbus-serialbattery/bms/seplos.py
@@ -79,12 +79,18 @@ 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:
- return self.read_status_data()
+ result = self.read_status_data()
except Exception as err:
logger.error(f"Unexpected {err=}, {type(err)=}")
- return False
+ result = False
+
+ # give the user a feedback that no BMS was found
+ if not result:
+ logger.error(">>> ERROR: No reply - returning")
+
+ return result
def get_settings(self):
# After successful connection get_settings will be called to set up the battery.
@@ -254,7 +260,7 @@ def is_valid_frame(data: bytes) -> bool:
* not checked: lchksum
"""
if len(data) < 18:
- logger.warning("short read, data={}".format(data))
+ logger.debug("short read, data={}".format(data))
return False
chksum = Seplos.get_checksum(data[1:-5])
diff --git a/etc/dbus-serialbattery/config.default.ini b/etc/dbus-serialbattery/config.default.ini
index 8018e3bd..e7c967ff 100644
--- a/etc/dbus-serialbattery/config.default.ini
+++ b/etc/dbus-serialbattery/config.default.ini
@@ -14,8 +14,11 @@ FLOAT_CELL_VOLTAGE = 3.375
; --------- Bluetooth BMS ---------
; Description: List the Bluetooth BMS here that you want to install
-; Example with 1 BMS: Jkbms_Ble C8:47:8C:00:00:00
-; Example with 3 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
+; -- Available Bluetooth BMS:
+; Jkbms_Ble, LltJbd_Ble
+; Example:
+; 1 BMS: Jkbms_Ble C8:47:8C:00:00:00
+; 3 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 =
; --------- BMS disconnect behaviour ---------
@@ -50,15 +53,18 @@ LINEAR_RECALCULATION_ON_PERC_CHANGE = 5
; 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 it switches back to max voltage.
+; 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
; 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.010V, then switch to float
; voltage of 53.6V after 300 additional seconds to don't stress the batteries. Allow max voltage of
-; 55.2V again if max cell difference is above 0.050V
+; 55.2V again if max cell difference is above 0.080V or SoC below 90%.
; Charge voltage control management enable (True/False).
CVCM_ENABLE = True
@@ -177,8 +183,12 @@ 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
+; Specify only one BMS type 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:
+; https://louisvdw.github.io/dbus-serialbattery/general/install#how-to-enable-a-disabled-bms
+; Ant, MNB, Sinowealth
BMS_TYPE =
; Publish the config settings to the dbus path "/Info/Config/"
@@ -196,10 +206,12 @@ MIDPOINT_ENABLE = False
; Battery temperature
-; Specifiy how the battery temperature is assembled
-; 0 Get mean of temperature sensor 1 and temperature sensor 2
+; Specify how the battery temperature is assembled
+; 0 Get mean of temperature sensor 1 to sensor 4
; 1 Get only temperature from temperature sensor 1
; 2 Get only temperature from temperature sensor 2
+; 3 Get only temperature from temperature sensor 3
+; 4 Get only temperature from temperature sensor 4
TEMP_BATTERY = 0
; Temperature sensor 1 name
@@ -208,6 +220,12 @@ TEMP_1_NAME = Temp 1
; Temperature sensor 2 name
TEMP_2_NAME = Temp 2
+; Temperature sensor 2 name
+TEMP_3_NAME = Temp 3
+
+; Temperature sensor 2 name
+TEMP_4_NAME = Temp 4
+
; --------- BMS specific settings ---------
@@ -218,7 +236,7 @@ SOC_LOW_WARNING = 20
SOC_LOW_ALARM = 10
; -- Daly settings
-; Battery capacity (amps) if the BMS does not support reading it
+; 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
@@ -228,3 +246,24 @@ GREENMETER_ADDRESS = 1
LIPRO_START_ADDRESS = 2
LIPRO_END_ADDRESS = 4
LIPRO_CELL_COUNT = 15
+
+; -- HeltecModbus (Heltec SmartBMS/YYBMS) settings
+; Set the Modbus addresses from the adapters
+; Separate each address to check by a comma like: 1, 2, 3, ...
+; factory default address will be 1
+HELTEC_MODBUS_ADDR = 1
+
+
+; --------- Battery monitor specific settings ---------
+; If you are using a SmartShunt or something else as a battery monitor, the battery voltage reported
+; from the BMS and SmartShunt could differ. This causes, that the driver never goapplies the float voltage,
+; since max voltage is never reached.
+; Example:
+; cell count: 16
+; MAX_CELL_VOLTAGE = 3.45
+; max voltage calculated = 16 * 3.45 = 55.20
+; CVL is set to 55.20 and the battery is now charged until the SmartShunt measures 55.20 V. The BMS
+; now measures 55.05 V since there is a voltage drop of 0.15 V. Since the dbus-serialbattery measures
+; 55.05 V the max voltage is never reached for the driver and max voltage is kept forever.
+; Set VOLTAGE_DROP to 0.15
+VOLTAGE_DROP = 0.00
diff --git a/etc/dbus-serialbattery/dbus-serialbattery.py b/etc/dbus-serialbattery/dbus-serialbattery.py
index 68265a07..4bca9b35 100644
--- a/etc/dbus-serialbattery/dbus-serialbattery.py
+++ b/etc/dbus-serialbattery/dbus-serialbattery.py
@@ -24,6 +24,7 @@
# import battery classes
from bms.daly import Daly
from bms.ecs import Ecs
+from bms.heltecmodbus import HeltecModbus
from bms.hlpdatabms4s import HLPdataBMS4S
from bms.jkbms import Jkbms
from bms.lifepower import Lifepower
@@ -39,6 +40,7 @@
{"bms": Daly, "baud": 9600, "address": b"\x40"},
{"bms": Daly, "baud": 9600, "address": b"\x80"},
{"bms": Ecs, "baud": 19200},
+ {"bms": HeltecModbus, "baud": 9600},
{"bms": HLPdataBMS4S, "baud": 9600},
{"bms": Jkbms, "baud": 115200},
{"bms": Lifepower, "baud": 9600},
@@ -56,7 +58,7 @@
if battery_type["bms"].__name__ == utils.BMS_TYPE or utils.BMS_TYPE == ""
]
-logger.info("")
+print("")
logger.info("Starting dbus-serialbattery")
@@ -72,17 +74,24 @@ def get_battery(_port) -> Union[Battery, None]:
while count > 0:
# create a new battery object that can read the battery and run connection test
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 " + battery.__class__.__name__
+ # noinspection PyBroadException
+ try:
+ logger.info("Testing " + test["bms"].__name__)
+ batteryClass = test["bms"]
+ baud = test["baud"]
+ battery: Battery = batteryClass(
+ port=_port, baud=baud, address=test.get("address")
)
- return battery
+ if battery.test_connection():
+ logger.info(
+ "Connection established to " + battery.__class__.__name__
+ )
+ return battery
+ except KeyboardInterrupt:
+ return None
+ except Exception:
+ # Ignore any malfunction test_function()
+ pass
count -= 1
sleep(0.5)
@@ -97,9 +106,7 @@ def get_port() -> str:
logger.info("No Port needed")
return "/dev/tty/USB9"
- logger.info(
- "dbus-serialbattery v" + str(utils.DRIVER_VERSION) + utils.DRIVER_SUBVERSION
- )
+ logger.info("dbus-serialbattery v" + str(utils.DRIVER_VERSION))
port = get_port()
battery = None
diff --git a/etc/dbus-serialbattery/dbushelper.py b/etc/dbus-serialbattery/dbushelper.py
index 555cb1ba..0cca3187 100644
--- a/etc/dbus-serialbattery/dbushelper.py
+++ b/etc/dbus-serialbattery/dbushelper.py
@@ -113,21 +113,17 @@ def setup_vedbus(self):
self._dbusservice.add_path(
"/Mgmt/ProcessVersion", "Python " + platform.python_version()
)
- self._dbusservice.add_path("/Mgmt/Connection", "Serial " + self.battery.port)
+ self._dbusservice.add_path("/Mgmt/Connection", self.battery.connection_name())
# Create the mandatory objects
self._dbusservice.add_path("/DeviceInstance", self.instance)
self._dbusservice.add_path("/ProductId", 0x0)
- self._dbusservice.add_path(
- "/ProductName", "SerialBattery(" + self.battery.type + ")"
- )
- self._dbusservice.add_path(
- "/FirmwareVersion", str(utils.DRIVER_VERSION) + utils.DRIVER_SUBVERSION
- )
+ self._dbusservice.add_path("/ProductName", self.battery.product_name())
+ self._dbusservice.add_path("/FirmwareVersion", str(utils.DRIVER_VERSION))
self._dbusservice.add_path("/HardwareVersion", self.battery.hardware_version)
self._dbusservice.add_path("/Connected", 1)
self._dbusservice.add_path(
- "/CustomName", "SerialBattery(" + self.battery.type + ")", writeable=True
+ "/CustomName", self.battery.custom_name(), writeable=True
)
self._dbusservice.add_path(
"/Serial", self.battery.unique_identifier, writeable=True
@@ -233,6 +229,10 @@ def setup_vedbus(self):
self._dbusservice.add_path("/System/MaxCellTemperature", None, writeable=True)
self._dbusservice.add_path("/System/MaxTemperatureCellId", None, writeable=True)
self._dbusservice.add_path("/System/MOSTemperature", None, writeable=True)
+ self._dbusservice.add_path("/System/Temperature1", None, writeable=True)
+ self._dbusservice.add_path("/System/Temperature2", None, writeable=True)
+ self._dbusservice.add_path("/System/Temperature3", None, writeable=True)
+ self._dbusservice.add_path("/System/Temperature4", None, writeable=True)
self._dbusservice.add_path(
"/System/MaxCellVoltage",
None,
@@ -253,6 +253,24 @@ def setup_vedbus(self):
self._dbusservice.add_path("/Io/AllowToCharge", 0, writeable=True)
self._dbusservice.add_path("/Io/AllowToDischarge", 0, writeable=True)
self._dbusservice.add_path("/Io/AllowToBalance", 0, writeable=True)
+ self._dbusservice.add_path(
+ "/Io/ForceChargingOff",
+ 0,
+ writeable=True,
+ onchangecallback=self.battery.force_charging_off_callback,
+ )
+ self._dbusservice.add_path(
+ "/Io/ForceDischargingOff",
+ 0,
+ writeable=True,
+ onchangecallback=self.battery.force_discharging_off_callback,
+ )
+ self._dbusservice.add_path(
+ "/Io/TurnBalancingOff",
+ 0,
+ writeable=True,
+ onchangecallback=self.battery.turn_balancing_off_callback,
+ )
# self._dbusservice.add_path('/SystemSwitch', 1, writeable=True)
# Create the alarms
@@ -454,6 +472,10 @@ def publish_dbus(self):
"/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/Temperature2"] = self.battery.temp2
+ self._dbusservice["/System/Temperature3"] = self.battery.temp3
+ self._dbusservice["/System/Temperature4"] = self.battery.temp4
# Voltage control
self._dbusservice["/Info/MaxChargeVoltage"] = self.battery.control_voltage
diff --git a/etc/dbus-serialbattery/disable.sh b/etc/dbus-serialbattery/disable.sh
index 34304cd5..f1902881 100755
--- a/etc/dbus-serialbattery/disable.sh
+++ b/etc/dbus-serialbattery/disable.sh
@@ -4,16 +4,27 @@
#set -x
# handle read only mounts
-sh /opt/victronenergy/swupdate-scripts/remount-rw.sh
+bash /opt/victronenergy/swupdate-scripts/remount-rw.sh
-# remove files, don't use variables here, since on an error the whole /opt/victronenergy gets deleted
+# remove driver from serial starter
rm -f /data/conf/serial-starter.d/dbus-serialbattery.conf
+# kill serial starter, to reload changes
+pkill -f "/opt/victronenergy/serial-starter/serial-starter.sh"
+
+# remove services
rm -rf /service/dbus-serialbattery.*
rm -rf /service/dbus-blebattery.*
+# kill driver, if running
+pkill -f "python .*/dbus-serialbattery.py"
+pkill -f "blebattery"
+
# remove install script from rc.local
sed -i "/bash \/data\/etc\/dbus-serialbattery\/reinstall-local.sh/d" /data/rc.local
+# remove cronjob
+sed -i "/5 0,12 \* \* \* \/etc\/init.d\/bluetooth restart/d" /var/spool/cron/root
+
### needed for upgrading from older versions | start ###
# remove old drivers before changing from dbus-blebattery-$1 to dbus-blebattery.$1
@@ -25,10 +36,5 @@ sed -i "/sh \/data\/etc\/dbus-serialbattery\/reinstall-local.sh/d" /data/rc.loca
sed -i "/sh \/data\/etc\/dbus-serialbattery\/installble.sh/d" /data/rc.local
### needed for upgrading from older versions | end ###
-
-# kill serial starter, to reload changes
-pkill -f "/opt/victronenergy/serial-starter/serial-starter.sh"
-
-# kill driver, if running
-pkill -f "serialbattery"
-pkill -f "blebattery"
+echo "The dbus-serialbattery driver was disabled".
+echo
diff --git a/etc/dbus-serialbattery/install-qml.sh b/etc/dbus-serialbattery/install-qml.sh
index 6c155b51..287aac83 100755
--- a/etc/dbus-serialbattery/install-qml.sh
+++ b/etc/dbus-serialbattery/install-qml.sh
@@ -7,7 +7,7 @@
# https://github.com/kwindrem/SetupHelper/blob/ebaa65fcf23e2bea6797f99c1c41174143c1153c/updateFileSets#L56-L81
function versionStringToNumber ()
{
- local local p4="" ; local p5="" ; local p5=""
+ local p4="" ; local p5="" ; local p5=""
local major=""; local minor=""
# first character should be 'v' so first awk parameter will be empty and is not prited into the read command
diff --git a/etc/dbus-serialbattery/install.sh b/etc/dbus-serialbattery/install.sh
old mode 100755
new mode 100644
index 17a12ed3..c54c92c6
--- a/etc/dbus-serialbattery/install.sh
+++ b/etc/dbus-serialbattery/install.sh
@@ -49,8 +49,8 @@ fi
## specific version
if [ "$version" = "specific version" ]; then
# read the url
- read -p "Enter the url of the \"venus-data.tar.gz\" you want to install: " tar_url
- wget -O /tmp/venus-data.tar.gz $tar_url
+ read -r -p "Enter the url of the \"venus-data.tar.gz\" you want to install: " tar_url
+ wget -O /tmp/venus-data.tar.gz "$tar_url"
if [ $? -ne 0 ]; then
echo "Error during downloading the TAR file. Please check, if the URL is correct."
exit
diff --git a/etc/dbus-serialbattery/qml/PageBatteryCellVoltages.qml b/etc/dbus-serialbattery/qml/PageBatteryCellVoltages.qml
index 112564df..ea723a89 100644
--- a/etc/dbus-serialbattery/qml/PageBatteryCellVoltages.qml
+++ b/etc/dbus-serialbattery/qml/PageBatteryCellVoltages.qml
@@ -4,6 +4,8 @@ import com.victron.velib 1.0
MbPage {
id: root
property string bindPrefix
+ property MbStyle style: MbStyle{}
+
property VBusItem _b1: VBusItem { bind: service.path("/Balances/Cell1") }
property VBusItem _b2: VBusItem { bind: service.path("/Balances/Cell2") }
property VBusItem _b3: VBusItem { bind: service.path("/Balances/Cell3") }
@@ -52,30 +54,30 @@ MbPage {
property VBusItem volt22: VBusItem { bind: service.path("/Voltages/Cell22") }
property VBusItem volt23: VBusItem { bind: service.path("/Voltages/Cell23") }
property VBusItem volt24: VBusItem { bind: service.path("/Voltages/Cell24") }
- property string c1: _b1.valid && _b1.text == "1" ? "#ff0000" : "#ddd"
- property string c2: _b2.valid && _b2.text == "1" ? "#ff0000" : "#ddd"
- property string c3: _b3.valid && _b3.text == "1" ? "#ff0000" : "#ddd"
- property string c4: _b4.valid && _b4.text == "1" ? "#ff0000" : "#ddd"
- property string c5: _b5.valid && _b5.text == "1" ? "#ff0000" : "#ddd"
- property string c6: _b6.valid && _b6.text == "1" ? "#ff0000" : "#ddd"
- property string c7: _b7.valid && _b7.text == "1" ? "#ff0000" : "#ddd"
- property string c8: _b8.valid && _b8.text == "1" ? "#ff0000" : "#ddd"
- property string c9: _b9.valid && _b9.text == "1" ? "#ff0000" : "#ddd"
- property string c10: _b10.valid && _b10.text == "1" ? "#ff0000" : "#ddd"
- property string c11: _b11.valid && _b11.text == "1" ? "#ff0000" : "#ddd"
- property string c12: _b12.valid && _b12.text == "1" ? "#ff0000" : "#ddd"
- property string c13: _b13.valid && _b13.text == "1" ? "#ff0000" : "#ddd"
- property string c14: _b14.valid && _b14.text == "1" ? "#ff0000" : "#ddd"
- property string c15: _b15.valid && _b15.text == "1" ? "#ff0000" : "#ddd"
- property string c16: _b16.valid && _b16.text == "1" ? "#ff0000" : "#ddd"
- property string c17: _b17.valid && _b17.text == "1" ? "#ff0000" : "#ddd"
- property string c18: _b18.valid && _b18.text == "1" ? "#ff0000" : "#ddd"
- property string c19: _b19.valid && _b19.text == "1" ? "#ff0000" : "#ddd"
- property string c20: _b20.valid && _b20.text == "1" ? "#ff0000" : "#ddd"
- property string c21: _b21.valid && _b21.text == "1" ? "#ff0000" : "#ddd"
- property string c22: _b22.valid && _b22.text == "1" ? "#ff0000" : "#ddd"
- property string c23: _b23.valid && _b23.text == "1" ? "#ff0000" : "#ddd"
- property string c24: _b24.valid && _b24.text == "1" ? "#ff0000" : "#ddd"
+ property string c1: _b1.valid && _b1.text == "1" ? "#ff0000" : style.borderColor
+ property string c2: _b2.valid && _b2.text == "1" ? "#ff0000" : style.borderColor
+ property string c3: _b3.valid && _b3.text == "1" ? "#ff0000" : style.borderColor
+ property string c4: _b4.valid && _b4.text == "1" ? "#ff0000" : style.borderColor
+ property string c5: _b5.valid && _b5.text == "1" ? "#ff0000" : style.borderColor
+ property string c6: _b6.valid && _b6.text == "1" ? "#ff0000" : style.borderColor
+ property string c7: _b7.valid && _b7.text == "1" ? "#ff0000" : style.borderColor
+ property string c8: _b8.valid && _b8.text == "1" ? "#ff0000" : style.borderColor
+ property string c9: _b9.valid && _b9.text == "1" ? "#ff0000" : style.borderColor
+ property string c10: _b10.valid && _b10.text == "1" ? "#ff0000" : style.borderColor
+ property string c11: _b11.valid && _b11.text == "1" ? "#ff0000" : style.borderColor
+ property string c12: _b12.valid && _b12.text == "1" ? "#ff0000" : style.borderColor
+ property string c13: _b13.valid && _b13.text == "1" ? "#ff0000" : style.borderColor
+ property string c14: _b14.valid && _b14.text == "1" ? "#ff0000" : style.borderColor
+ property string c15: _b15.valid && _b15.text == "1" ? "#ff0000" : style.borderColor
+ property string c16: _b16.valid && _b16.text == "1" ? "#ff0000" : style.borderColor
+ property string c17: _b17.valid && _b17.text == "1" ? "#ff0000" : style.borderColor
+ property string c18: _b18.valid && _b18.text == "1" ? "#ff0000" : style.borderColor
+ property string c19: _b19.valid && _b19.text == "1" ? "#ff0000" : style.borderColor
+ property string c20: _b20.valid && _b20.text == "1" ? "#ff0000" : style.borderColor
+ property string c21: _b21.valid && _b21.text == "1" ? "#ff0000" : style.borderColor
+ property string c22: _b22.valid && _b22.text == "1" ? "#ff0000" : style.borderColor
+ property string c23: _b23.valid && _b23.text == "1" ? "#ff0000" : style.borderColor
+ property string c24: _b24.valid && _b24.text == "1" ? "#ff0000" : style.borderColor
title: service.description + " | Cell Voltages"
model: VisibleItemModel {
diff --git a/etc/dbus-serialbattery/qml/PageLynxIonIo.qml b/etc/dbus-serialbattery/qml/PageLynxIonIo.qml
index e6ad7106..9d0ff4c4 100644
--- a/etc/dbus-serialbattery/qml/PageLynxIonIo.qml
+++ b/etc/dbus-serialbattery/qml/PageLynxIonIo.qml
@@ -49,6 +49,24 @@ MbPage {
]
}
+ MbSwitch {
+ name: qsTr("Force charging off")
+ bind: Utils.path(bindPrefix, "/Io/ForceChargingOff")
+ show: item.valid
+ }
+
+ MbSwitch {
+ name: qsTr("Force discharging off")
+ bind: Utils.path(bindPrefix, "/Io/ForceDischargingOff")
+ show: item.valid
+ }
+
+ MbSwitch {
+ name: qsTr("Turn balancing off")
+ bind: Utils.path(bindPrefix, "/Io/TurnBalancingOff")
+ show: item.valid
+ }
+
MbItemOptions {
description: qsTr("External relay")
bind: Utils.path(bindPrefix, "/Io/ExternalRelay")
@@ -70,5 +88,6 @@ MbPage {
MbOption{description: qsTr("Active"); value: 1}
]
}
+
}
}
diff --git a/etc/dbus-serialbattery/reinstall-local.sh b/etc/dbus-serialbattery/reinstall-local.sh
index f822e026..a518a100 100755
--- a/etc/dbus-serialbattery/reinstall-local.sh
+++ b/etc/dbus-serialbattery/reinstall-local.sh
@@ -5,6 +5,63 @@
DRIVERNAME=dbus-serialbattery
+
+# check if minimum required Venus OS is installed | start
+versionRequired="v2.90"
+
+# elaborate version string for better comparing
+# https://github.com/kwindrem/SetupHelper/blob/ebaa65fcf23e2bea6797f99c1c41174143c1153c/updateFileSets#L56-L81
+function versionStringToNumber ()
+{
+ local p4="" ; local p5="" ; local p5=""
+ local major=""; local minor=""
+
+ # first character should be 'v' so first awk parameter will be empty and is not prited into the read command
+ #
+ # version number formats: v2.40, v2.40~6, v2.40-large-7, v2.40~6-large-7
+ # so we must adjust how we use paramters read from the version string
+ # and parsed by awk
+ # if no beta make sure release is greater than any beta (i.e., a beta portion of 999)
+
+ read major minor p4 p5 p6 <<< $(echo $1 | awk -v FS='[v.~-]' '{print $2, $3, $4, $5, $6}')
+ ((versionNumber = major * 1000000000 + minor * 1000000))
+ if [ -z $p4 ] || [ $p4 = "large" ]; then
+ ((versionNumber += 999))
+ else
+ ((versionNumber += p4))
+ fi
+ if [ ! -z $p4 ] && [ $p4 = "large" ]; then
+ ((versionNumber += p5 * 1000))
+ large=$p5
+ elif [ ! -z $p6 ]; then
+ ((versionNumber += p6 * 1000))
+ fi
+}
+
+# get current Venus OS version
+versionStringToNumber "$(head -n 1 /opt/victronenergy/version)"
+venusVersionNumber="$versionNumber"
+
+# minimum required version to install the driver
+versionStringToNumber "$versionRequired"
+
+if (( $venusVersionNumber < $versionNumber )); then
+ echo
+ echo
+ echo "Minimum required Venus OS version \"$versionRequired\" not met. Currently version \"$(head -n 1 /opt/victronenergy/version)\" is installed."
+ echo
+ echo "Please update via \"Remote Console/GUI -> Settings -> Firmware -> Online Update\""
+ echo "OR"
+ echo "by executing \"/opt/victronenergy/swupdate-scripts/check-updates.sh -update -force\""
+ echo
+ echo "Install the driver again after Venus OS was updated."
+ echo
+ echo
+ exit 1
+fi
+# check if minimum required Venus OS is installed | end
+
+
# handle read only mounts
bash /opt/victronenergy/swupdate-scripts/remount-rw.sh
@@ -24,38 +81,47 @@ serialstarter_path="/data/conf/serial-starter.d"
serialstarter_file="$serialstarter_path/dbus-serialbattery.conf"
# check if folder is a file (older versions of this driver < v1.0.0)
-if [ -f $serialstarter_path ]; then
- rm -f $serialstarter_path
+if [ -f "$serialstarter_path" ]; then
+ rm -f "$serialstarter_path"
fi
# check if folder exists
-if [ ! -d $serialstarter_path ]; then
- mkdir $serialstarter_path
+if [ ! -d "$serialstarter_path" ]; then
+ mkdir "$serialstarter_path"
fi
# check if file exists
-if [ ! -f $serialstarter_file ]; then
- echo "service sbattery dbus-serialbattery" >> $serialstarter_file
- echo "alias default gps:vedirect:sbattery" >> $serialstarter_file
- echo "alias rs485 cgwacs:fzsonick:imt:modbus:sbattery" >> $serialstarter_file
+if [ ! -f "$serialstarter_file" ]; then
+ {
+ echo "service sbattery dbus-serialbattery"
+ echo "alias default gps:vedirect:sbattery"
+ echo "alias rs485 cgwacs:fzsonick:imt:modbus:sbattery"
+ } > "$serialstarter_file"
fi
# add install-script to rc.local to be ready for firmware update
filename=/data/rc.local
-if [ ! -f $filename ]; then
- echo "#!/bin/bash" >> $filename
- chmod 755 $filename
+if [ ! -f "$filename" ]; then
+ echo "#!/bin/bash" > "$filename"
+ chmod 755 "$filename"
fi
grep -qxF "bash /data/etc/$DRIVERNAME/reinstall-local.sh" $filename || echo "bash /data/etc/$DRIVERNAME/reinstall-local.sh" >> $filename
# add empty config.ini, if it does not exist to make it easier for users to add custom settings
-filename=/data/etc/$DRIVERNAME/config.ini
-if [ ! -f $filename ]; then
- echo "[DEFAULT]" > $filename
- echo "" >> $filename
- echo "; If you want to add custom settings, then check the settings you want to change in \"config.default.ini\"" >> $filename
- echo "; and add them below to persist future driver updates." >> $filename
- echo "" >> $filename
+filename="/data/etc/$DRIVERNAME/config.ini"
+if [ ! -f "$filename" ]; then
+ {
+ echo "[DEFAULT]"
+ echo
+ echo "; If you want to add custom values/settings, then check the values/settings you want to change in \"config.default.ini\""
+ echo "; and insert them below to persist future driver updates."
+ echo
+ echo "; Example (remove the semicolon \";\" to uncomment and activate the value/setting):"
+ echo "; MAX_BATTERY_CURRENT = 50.0"
+ echo "; MAX_BATTERY_DISCHARGE_CURRENT = 60.0"
+ echo
+ echo
+ } > $filename
fi
@@ -84,7 +150,7 @@ rm -rf /service/dbus-blebattery.*
# kill all blebattery processes
pkill -f "blebattery"
-if [ $length -gt 0 ]; then
+if [ "$length" -gt 0 ]; then
echo "Found $length Bluetooth BMS in the config file!"
echo ""
@@ -101,16 +167,20 @@ if [ $length -gt 0 ]; then
# function to install ble battery
install_blebattery_service() {
- mkdir -p /service/dbus-blebattery.$1/log
- 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
- echo "python /opt/victronenergy/dbus-serialbattery/dbus-serialbattery.py $2 $3" >> /service/dbus-blebattery.$1/run
- chmod 755 /service/dbus-blebattery.$1/run
+ mkdir -p "/service/dbus-blebattery.$1/log"
+ {
+ echo "#!/bin/sh"
+ 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"
+ echo "exec 2>&1"
+ echo "bluetoothctl disconnect $3"
+ echo "python /opt/victronenergy/dbus-serialbattery/dbus-serialbattery.py $2 $3"
+ } > "/service/dbus-blebattery.$1/run"
+ chmod 755 "/service/dbus-blebattery.$1/run"
}
echo "Packages installed."
@@ -119,7 +189,7 @@ if [ $length -gt 0 ]; then
# install_blebattery_service 0 Jkbms_Ble C8:47:8C:00:00:00
# install_blebattery_service 1 Jkbms_Ble C8:47:8C:00:00:11
- for (( i=0; i<${length}; i++ ));
+ for (( i=0; i