From dbb96042ddd5433b5896b991aec2f7a47148a814 Mon Sep 17 00:00:00 2001 From: Raphael Mack Date: Sun, 21 May 2023 19:02:43 +0200 Subject: [PATCH 01/15] initial support for Heltec SmartBMS/YY-BMS --- etc/dbus-serialbattery/bms/heltecmodbus.py | 332 +++++++++++++++++++ etc/dbus-serialbattery/dbus-serialbattery.py | 4 + 2 files changed, 336 insertions(+) create mode 100644 etc/dbus-serialbattery/bms/heltecmodbus.py diff --git a/etc/dbus-serialbattery/bms/heltecmodbus.py b/etc/dbus-serialbattery/bms/heltecmodbus.py new file mode 100644 index 00000000..78e4794b --- /dev/null +++ b/etc/dbus-serialbattery/bms/heltecmodbus.py @@ -0,0 +1,332 @@ +# -*- 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 Protection, Battery, Cell +from utils import * +from struct import * +import time +import minimalmodbus +from typing import Dict +import threading + +RETRYCNT = 10 # 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 + +SLPTIME = 0.02 # 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 + +mbdevs: Dict[int, minimalmodbus.Instrument] = {} +locks: Dict[int, any] = {} + +class HeltecModbus(Battery): + def __init__(self, port, baud, address): + self.address = address + super(HeltecModbus, self).__init__(port, baud, address) + if address != 0: + type = "#" + str(address) + "_" + else: + type = "" + self.type = 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 + logger.info("Testing on slave address " + str(self.address)) + if not self.address in locks: + locks[self.address] = threading.Lock() + + 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 + mbdev.serial.timeout = 0.4 # yes, 400ms is long but the BMS is sometimes really slow in responding, so this seems a good compromize + mbdevs[self.address] = mbdev + + for n in range(1, RETRYCNT): + try: + string = mbdev.read_string(7, 13) + time.sleep(SLPTIME) + result = True + logger.info("found in try " + str(n) + "/" + str(RETRYCNT) + " for " + self.port + "(" + str(self.address) + "): " + string) + except Exception as e: + logger.warn("testing failed (" + str(e) + ") " + str(n) + "/" + str(RETRYCNT) + " for " + self.port + "(" + str(self.address) + ")") + continue + break + + return result 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): + 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)) + 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 + self.protection.voltage_high = 1 # we handle a single cell OV as total OV, as long as cell_high is not explicitly handled + 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, 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 == 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/dbus-serialbattery.py b/etc/dbus-serialbattery/dbus-serialbattery.py index 68265a07..4eac2e42 100644 --- a/etc/dbus-serialbattery/dbus-serialbattery.py +++ b/etc/dbus-serialbattery/dbus-serialbattery.py @@ -30,6 +30,7 @@ from bms.lltjbd import LltJbd from bms.renogy import Renogy from bms.seplos import Seplos +from bms.heltecmodbus import HeltecModbus # from bms.ant import Ant # from bms.mnb import MNB @@ -46,6 +47,9 @@ {"bms": Renogy, "baud": 9600, "address": b"\x30"}, {"bms": Renogy, "baud": 9600, "address": b"\xF7"}, {"bms": Seplos, "baud": 19200}, + # {"bms" : HeltecModbus, "baud" : 9600, "address" : 2}, + # {"bms" : HeltecModbus, "baud" : 9600, "address" : 1}, + {"bms" : HeltecModbus, "baud" : 9600, "address" : 0}, # {"bms": Ant, "baud": 19200}, # {"bms": MNB, "baud": 9600}, # {"bms": Sinowealth}, From 78085500da1a31f71cd69acf63785d0e822e10af Mon Sep 17 00:00:00 2001 From: Raphael Mack Date: Sun, 21 May 2023 22:18:57 +0200 Subject: [PATCH 02/15] fix typo --- docs/docs/general/features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/general/features.md b/docs/docs/general/features.md index a7b2434c..9c71542f 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` From e095e1fdf8720e49d779207018cd9aa3090ab0f0 Mon Sep 17 00:00:00 2001 From: Raphael Mack Date: Sun, 21 May 2023 22:43:48 +0200 Subject: [PATCH 03/15] document support for Heltec --- docs/docs/general/features.md | 42 +++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/docs/general/features.md b/docs/docs/general/features.md index 9c71542f..dca4f8ba 100644 --- a/docs/docs/general/features.md +++ b/docs/docs/general/features.md @@ -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 | ? | Yes | Calc | Calc | Calc | Yes | Calc | Calc | Calc | `Calc` means that the value is calculated by the driver. From 112ba6eb6ae6b906e2c1f9cb9c4aaea214bac38d Mon Sep 17 00:00:00 2001 From: Raphael Mack Date: Sun, 21 May 2023 23:36:26 +0200 Subject: [PATCH 04/15] fix lint and default address --- etc/dbus-serialbattery/dbus-serialbattery.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/etc/dbus-serialbattery/dbus-serialbattery.py b/etc/dbus-serialbattery/dbus-serialbattery.py index 4eac2e42..3de1d598 100644 --- a/etc/dbus-serialbattery/dbus-serialbattery.py +++ b/etc/dbus-serialbattery/dbus-serialbattery.py @@ -47,9 +47,9 @@ {"bms": Renogy, "baud": 9600, "address": b"\x30"}, {"bms": Renogy, "baud": 9600, "address": b"\xF7"}, {"bms": Seplos, "baud": 19200}, - # {"bms" : HeltecModbus, "baud" : 9600, "address" : 2}, - # {"bms" : HeltecModbus, "baud" : 9600, "address" : 1}, - {"bms" : HeltecModbus, "baud" : 9600, "address" : 0}, + # {"bms": HeltecModbus, "baud" : 9600, "address" : 3}, + # {"bms": HeltecModbus, "baud" : 9600, "address" : 2}, + {"bms": HeltecModbus, "baud" : 9600, "address" : 1}, # {"bms": Ant, "baud": 19200}, # {"bms": MNB, "baud": 9600}, # {"bms": Sinowealth}, From 6ab8f906ec956a4f21f1da66f3cd2bf2e7110320 Mon Sep 17 00:00:00 2001 From: Raphael Mack Date: Sun, 21 May 2023 23:36:44 +0200 Subject: [PATCH 05/15] fix lint --- etc/dbus-serialbattery/bms/heltecmodbus.py | 194 +++++++++++++++------ 1 file changed, 142 insertions(+), 52 deletions(-) diff --git a/etc/dbus-serialbattery/bms/heltecmodbus.py b/etc/dbus-serialbattery/bms/heltecmodbus.py index 78e4794b..e69ac68b 100644 --- a/etc/dbus-serialbattery/bms/heltecmodbus.py +++ b/etc/dbus-serialbattery/bms/heltecmodbus.py @@ -13,13 +13,14 @@ from typing import Dict import threading -RETRYCNT = 10 # 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 Heltec BMS is not always as responsive as it should, so let's try it up to (RETRYCNT - 1) times to talk to it -SLPTIME = 0.02 # 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.02 # 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 mbdevs: Dict[int, minimalmodbus.Instrument] = {} locks: Dict[int, any] = {} + class HeltecModbus(Battery): def __init__(self, port, baud, address): self.address = address @@ -35,29 +36,63 @@ def test_connection(self): # The result or call should be unique to this BMS. Battery name or version, etc. # Return True if success, False for failure logger.info("Testing on slave address " + str(self.address)) + found = False if not self.address in locks: locks[self.address] = threading.Lock() with locks[self.address]: - mbdev = minimalmodbus.Instrument(self.port, slaveaddress=self.address, mode="rtu", close_port_after_each_call=True, debug=False) + 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 - mbdev.serial.timeout = 0.4 # yes, 400ms is long but the BMS is sometimes really slow in responding, so this seems a good compromize + mbdev.serial.timeout = 0.4 # yes, 400ms is long but the BMS is sometimes really slow in responding, so this seems a good compromize mbdevs[self.address] = mbdev - + for n in range(1, RETRYCNT): try: string = mbdev.read_string(7, 13) time.sleep(SLPTIME) - result = True - logger.info("found in try " + str(n) + "/" + str(RETRYCNT) + " for " + self.port + "(" + str(self.address) + "): " + string) + found = True + logger.info( + "found in try " + + str(n) + + "/" + + str(RETRYCNT) + + " for " + + self.port + + "(" + + str(self.address) + + "): " + + string + ) except Exception as e: - logger.warn("testing failed (" + str(e) + ") " + str(n) + "/" + str(RETRYCNT) + " for " + self.port + "(" + str(self.address) + ")") + logger.warn( + "testing failed (" + + str(e) + + ") " + + str(n) + + "/" + + str(RETRYCNT) + + " for " + + self.port + + "(" + + str(self.address) + + ")") continue break - return result and self.read_status_data() and self.get_settings() and self.refresh_data() + 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 @@ -65,14 +100,12 @@ def get_settings(self): 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] @@ -80,31 +113,43 @@ def read_status_data(self): for n in range(1, RETRYCNT): try: ccur = mbdev.read_register(191, 0, 3, False) - self.max_battery_charge_current = ((int)(((ccur & 0xff) << 8) | ((ccur >> 8) & 0xff))) / 100 + 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 + 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 + 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 + 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 + 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 + 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 + self.min_cell_voltage = ( + ((volt & 0xFF) << 8) | ((volt >> 8) & 0xFF) + ) / 1000 time.sleep(SLPTIME) string = mbdev.read_string(7, 13) @@ -116,17 +161,20 @@ def read_status_data(self): time.sleep(SLPTIME) serial1 = mbdev.read_registers(2, number_of_registers=4) - self.unique_identifier = '-'.join('{:04x}'.format(x) for x in serial1) + 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 + # 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: @@ -137,11 +185,22 @@ def read_status_data(self): self.cellType = "unknown" time.sleep(SLPTIME) - self.hardware_version = self.devName + "(" + str((mbdev.read_register(38)>>8) & 0xff) + ")" + 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) + 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 @@ -171,13 +230,21 @@ def read_soc_data(self): for n in range(1, RETRYCNT): try: - self.voltage = mbdev.read_long(76, 3, True, minimalmodbus.BYTEORDER_LITTLE) / 1000 + 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 + 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) + runState1 = mbdev.read_long( + 152, 3, True, minimalmodbus.BYTEORDER_LITTLE + ) time.sleep(SLPTIME) # bit 29 is discharge protection @@ -192,15 +259,19 @@ def read_soc_data(self): 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 + 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 - self.protection.voltage_high = 1 # we handle a single cell OV as total OV, as long as cell_high is not explicitly handled + self.protection.voltage_high = 1 # we handle a single cell OV as total OV, as long as cell_high is not explicitly handled else: self.protection.voltage_cell_high = 0 @@ -208,7 +279,7 @@ def read_soc_data(self): self.protection.voltage_cell_low = 2 else: self.protection.voltage_cell_low = 0 - + if warnings & (1 << 4): self.protection.voltage_low = 2 else: @@ -226,7 +297,7 @@ def read_soc_data(self): else: self.protection.current_under = 0 - if warnings & (1 << 8): # this is a short circuit + if warnings & (1 << 8): # this is a short circuit self.protection.current_over = 2 if warnings & (1 << 9): @@ -249,39 +320,39 @@ def read_soc_data(self): else: self.protection.temp_low_discharge = 0 - if warnings & (1 << 13): # MOS overtemp + 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 + if warnings & (1 << 14): # SOC low self.protection.soc_low = 2 else: self.protection.soc_low = 0 - if warnings & (0xffff0000): # any other fault + 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 + 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) + # 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 + 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 + most = (temps & 0xFF) - 40 + balt = ((temps >> 8) & 0xFF) - 40 # balancer temperature is not handled separately, so let's display the max of both temperatures inside the BMS as mos temperature self.temp_mos = max(most, balt) time.sleep(SLPTIME) @@ -289,7 +360,14 @@ def read_soc_data(self): return True except Exception as e: - logger.warn("Error reading SOC, retry (" + str(n) + "/" + str(RETRYCNT) + ") " + str(e)) + logger.warn( + "Error reading SOC, retry (" + + str(n) + + "/" + + str(RETRYCNT) + + ") " + + str(e) + ) continue break logger.warn("Error reading SOC, failed") @@ -303,14 +381,26 @@ def read_cell_data(self): for n in range(1, RETRYCNT): try: - cells = mbdev.read_registers(81, number_of_registers=self.cell_count) + 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) + + 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)) + logger.warn( + "read_cell_data() failed (" + + str(e) + + ") " + + str(n) + + "/" + + str(RETRYCNT) + ) continue break if result == False: @@ -323,9 +413,9 @@ def read_cell_data(self): i = 0 for cell in cells: - cellV = ((cell & 0xff) << 8) | ((cell >> 8) & 0xff) + cellV = ((cell & 0xFF) << 8) | ((cell >> 8) & 0xFF) self.cells[i].voltage = cellV / 1000 - self.cells[i].balance = ((balancing & (1 << i)) != 0) + self.cells[i].balance = (balancing & (1 << i) != 0) i = i + 1 From cdd02d61458c9481552be3b74d66501440fc2c15 Mon Sep 17 00:00:00 2001 From: Raphael Mack Date: Sun, 21 May 2023 23:47:47 +0200 Subject: [PATCH 06/15] further lint fixes --- etc/dbus-serialbattery/bms/heltecmodbus.py | 30 ++++++++++++-------- etc/dbus-serialbattery/dbus-serialbattery.py | 2 +- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/etc/dbus-serialbattery/bms/heltecmodbus.py b/etc/dbus-serialbattery/bms/heltecmodbus.py index e69ac68b..8a2dee61 100644 --- a/etc/dbus-serialbattery/bms/heltecmodbus.py +++ b/etc/dbus-serialbattery/bms/heltecmodbus.py @@ -14,7 +14,7 @@ import threading RETRYCNT = 10 # 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 - + SLPTIME = 0.02 # 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 mbdevs: Dict[int, minimalmodbus.Instrument] = {} @@ -83,7 +83,8 @@ def test_connection(self): + self.port + "(" + str(self.address) - + ")") + + ")" + ) continue break @@ -93,7 +94,7 @@ def test_connection(self): 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 @@ -162,7 +163,7 @@ def read_status_data(self): serial1 = mbdev.read_registers(2, number_of_registers=4) self.unique_identifier = '-'.join( - '{:04x}'.format(x) for x in serial1 + "{:04x}".format(x) for x in serial1 ) time.sleep(SLPTIME) @@ -172,7 +173,7 @@ def read_status_data(self): 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: @@ -206,7 +207,14 @@ def read_status_data(self): # 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)) + logger.warn( + "Error reading settings from BMS, retry (" + + str(n) + + "/" + + str(RETRYCNT) + + "): " + + str(e) + ) continue logger.info(self.hardware_version) @@ -229,7 +237,6 @@ def read_soc_data(self): with locks[self.address]: for n in range(1, RETRYCNT): try: - self.voltage = ( mbdev.read_long(76, 3, True, minimalmodbus.BYTEORDER_LITTLE) / 1000 @@ -264,7 +271,7 @@ def read_soc_data(self): ) if (warnings & (1 << 3)) or ( warnings & (1 << 15) - ): # 15 is full protection, 3 is total overvoltage + ): # 15 is full protection, 3 is total overvoltage self.voltage_high = 2 else: self.voltage_high = 0 @@ -299,7 +306,7 @@ def read_soc_data(self): 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: @@ -380,12 +387,11 @@ def read_cell_data(self): 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 ) @@ -415,7 +421,7 @@ def read_cell_data(self): 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) + self.cells[i].balance = balancing & (1 << i) != 0 i = i + 1 diff --git a/etc/dbus-serialbattery/dbus-serialbattery.py b/etc/dbus-serialbattery/dbus-serialbattery.py index 3de1d598..074f11ef 100644 --- a/etc/dbus-serialbattery/dbus-serialbattery.py +++ b/etc/dbus-serialbattery/dbus-serialbattery.py @@ -49,7 +49,7 @@ {"bms": Seplos, "baud": 19200}, # {"bms": HeltecModbus, "baud" : 9600, "address" : 3}, # {"bms": HeltecModbus, "baud" : 9600, "address" : 2}, - {"bms": HeltecModbus, "baud" : 9600, "address" : 1}, + {"bms": HeltecModbus, "baud": 9600, "address" : 1}, # {"bms": Ant, "baud": 19200}, # {"bms": MNB, "baud": 9600}, # {"bms": Sinowealth}, From ef1a14dd5dffcb3cfce5a23a8f1699cfe3a32243 Mon Sep 17 00:00:00 2001 From: Raphael Mack Date: Sun, 21 May 2023 23:55:55 +0200 Subject: [PATCH 07/15] next try :-( --- etc/dbus-serialbattery/bms/heltecmodbus.py | 6 +++--- etc/dbus-serialbattery/dbus-serialbattery.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/etc/dbus-serialbattery/bms/heltecmodbus.py b/etc/dbus-serialbattery/bms/heltecmodbus.py index 8a2dee61..054cf144 100644 --- a/etc/dbus-serialbattery/bms/heltecmodbus.py +++ b/etc/dbus-serialbattery/bms/heltecmodbus.py @@ -87,7 +87,7 @@ def test_connection(self): ) continue break - + return ( found and self.read_status_data() @@ -162,7 +162,7 @@ def read_status_data(self): time.sleep(SLPTIME) serial1 = mbdev.read_registers(2, number_of_registers=4) - self.unique_identifier = '-'.join( + self.unique_identifier = "-".join( "{:04x}".format(x) for x in serial1 ) time.sleep(SLPTIME) @@ -203,7 +203,7 @@ def read_status_data(self): + 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: diff --git a/etc/dbus-serialbattery/dbus-serialbattery.py b/etc/dbus-serialbattery/dbus-serialbattery.py index 074f11ef..77489b1a 100644 --- a/etc/dbus-serialbattery/dbus-serialbattery.py +++ b/etc/dbus-serialbattery/dbus-serialbattery.py @@ -47,9 +47,9 @@ {"bms": Renogy, "baud": 9600, "address": b"\x30"}, {"bms": Renogy, "baud": 9600, "address": b"\xF7"}, {"bms": Seplos, "baud": 19200}, - # {"bms": HeltecModbus, "baud" : 9600, "address" : 3}, - # {"bms": HeltecModbus, "baud" : 9600, "address" : 2}, - {"bms": HeltecModbus, "baud": 9600, "address" : 1}, + # {"bms": HeltecModbus, "baud": 9600, "address": 3}, + # {"bms": HeltecModbus, "baud": 9600, "address": 2}, + {"bms": HeltecModbus, "baud": 9600, "address": 1}, # {"bms": Ant, "baud": 19200}, # {"bms": MNB, "baud": 9600}, # {"bms": Sinowealth}, From e44498123630e2902dcb62744a1af3f71f023507 Mon Sep 17 00:00:00 2001 From: Raphael Mack Date: Mon, 22 May 2023 22:35:32 +0200 Subject: [PATCH 08/15] rework from review (update docs, generalize configuration of modbus address) --- docs/docs/general/features.md | 2 +- docs/docs/general/install.md | 2 +- docs/docs/general/supported-bms.md | 3 + etc/dbus-serialbattery/bms/heltecmodbus.py | 112 ++++++++++--------- etc/dbus-serialbattery/config.default.ini | 6 + etc/dbus-serialbattery/dbus-serialbattery.py | 6 +- etc/dbus-serialbattery/utils.py | 4 + 7 files changed, 75 insertions(+), 60 deletions(-) diff --git a/docs/docs/general/features.md b/docs/docs/general/features.md index dca4f8ba..2ed2e9b3 100644 --- a/docs/docs/general/features.md +++ b/docs/docs/general/features.md @@ -141,7 +141,7 @@ If the `MAX_CELL_VOLTAGE` \* `cell count` is reached for `MAX_VOLTAGE_TIME_SEC` | 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 | ? | Yes | Calc | Calc | Calc | Yes | Calc | Calc | Calc | +| 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..0a0a87aa 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 → set INVERT_CURRENT_MEASUREMENT = -1 and 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/bms/heltecmodbus.py b/etc/dbus-serialbattery/bms/heltecmodbus.py index 054cf144..4ab5b9d4 100644 --- a/etc/dbus-serialbattery/bms/heltecmodbus.py +++ b/etc/dbus-serialbattery/bms/heltecmodbus.py @@ -6,8 +6,9 @@ from battery import Protection, Battery, Cell -from utils import * -from struct import * +from utils import logger +import utils +import serial import time import minimalmodbus from typing import Dict @@ -35,58 +36,61 @@ 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 - logger.info("Testing on slave address " + str(self.address)) - found = False - if not self.address in locks: - locks[self.address] = threading.Lock() - - 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 - mbdev.serial.timeout = 0.4 # yes, 400ms is long but the BMS is sometimes really slow in responding, so this seems a good compromize - mbdevs[self.address] = mbdev - - for n in range(1, RETRYCNT): - try: - string = mbdev.read_string(7, 13) - time.sleep(SLPTIME) - found = True - logger.info( - "found in try " - + str(n) - + "/" - + str(RETRYCNT) - + " for " - + self.port - + "(" - + str(self.address) - + "): " - + string - ) - except Exception as e: - logger.warn( - "testing failed (" - + str(e) - + ") " - + str(n) - + "/" - + str(RETRYCNT) - + " for " - + self.port - + "(" - + str(self.address) - + ")" - ) - continue - break + for self.address in utils.HELTEC_MODBUS_ADDR: + logger.info("Testing on slave address " + str(self.address)) + found = False + if not self.address in locks: + locks[self.address] = threading.Lock() +# TODO: we need to lock not only based on the address, but based on the port, so 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 + mbdev.serial.timeout = 0.4 # yes, 400ms is long but the BMS is sometimes really slow in responding, so this seems a good compromize + mbdevs[self.address] = mbdev + + for n in range(1, RETRYCNT): + try: + string = mbdev.read_string(7, 13) + time.sleep(SLPTIME) + found = True + logger.info( + "found in try " + + str(n) + + "/" + + str(RETRYCNT) + + " for " + + self.port + + "(" + + str(self.address) + + "): " + + string + ) + except Exception as e: + logger.warn( + "testing failed (" + + str(e) + + ") " + + str(n) + + "/" + + str(RETRYCNT) + + " for " + + self.port + + "(" + + str(self.address) + + ")" + ) + continue + break + if found: break return ( found diff --git a/etc/dbus-serialbattery/config.default.ini b/etc/dbus-serialbattery/config.default.ini index 8018e3bd..5a8e270a 100644 --- a/etc/dbus-serialbattery/config.default.ini +++ b/etc/dbus-serialbattery/config.default.ini @@ -228,3 +228,9 @@ 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 \ No newline at end of file diff --git a/etc/dbus-serialbattery/dbus-serialbattery.py b/etc/dbus-serialbattery/dbus-serialbattery.py index 77489b1a..b0cbd134 100644 --- a/etc/dbus-serialbattery/dbus-serialbattery.py +++ b/etc/dbus-serialbattery/dbus-serialbattery.py @@ -24,13 +24,13 @@ # 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 from bms.lltjbd import LltJbd from bms.renogy import Renogy from bms.seplos import Seplos -from bms.heltecmodbus import HeltecModbus # from bms.ant import Ant # from bms.mnb import MNB @@ -40,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}, @@ -47,9 +48,6 @@ {"bms": Renogy, "baud": 9600, "address": b"\x30"}, {"bms": Renogy, "baud": 9600, "address": b"\xF7"}, {"bms": Seplos, "baud": 19200}, - # {"bms": HeltecModbus, "baud": 9600, "address": 3}, - # {"bms": HeltecModbus, "baud": 9600, "address": 2}, - {"bms": HeltecModbus, "baud": 9600, "address": 1}, # {"bms": Ant, "baud": 19200}, # {"bms": MNB, "baud": 9600}, # {"bms": Sinowealth}, diff --git a/etc/dbus-serialbattery/utils.py b/etc/dbus-serialbattery/utils.py index 74aa7d22..ebe2eb4c 100644 --- a/etc/dbus-serialbattery/utils.py +++ b/etc/dbus-serialbattery/utils.py @@ -304,6 +304,10 @@ def _get_list_from_config( LIPRO_END_ADDRESS = int(config["DEFAULT"]["LIPRO_END_ADDRESS"]) LIPRO_CELL_COUNT = int(config["DEFAULT"]["LIPRO_CELL_COUNT"]) +# -- HeltecModbus device settings +HELTEC_MODBUS_ADDR = _get_list_from_config( + "DEFAULT", "HELTEC_MODBUS_ADDR", lambda v: int(v) +) # --------- Functions --------- def constrain(val, min_val, max_val): From 1df8d17558169664e18ee1da5420ae66d0ac214d Mon Sep 17 00:00:00 2001 From: Raphael Mack Date: Mon, 22 May 2023 22:51:24 +0200 Subject: [PATCH 09/15] further lint improvements --- etc/dbus-serialbattery/bms/heltecmodbus.py | 34 +++++++++++++--------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/etc/dbus-serialbattery/bms/heltecmodbus.py b/etc/dbus-serialbattery/bms/heltecmodbus.py index 4ab5b9d4..8a66ccd7 100644 --- a/etc/dbus-serialbattery/bms/heltecmodbus.py +++ b/etc/dbus-serialbattery/bms/heltecmodbus.py @@ -2,10 +2,11 @@ # 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) +# - 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 Protection, Battery, Cell +from battery import Battery, Cell from utils import logger import utils import serial @@ -14,9 +15,12 @@ from typing import Dict import threading -RETRYCNT = 10 # 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 +# 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 -SLPTIME = 0.02 # 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 +# 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] = {} @@ -39,7 +43,7 @@ def test_connection(self): for self.address in utils.HELTEC_MODBUS_ADDR: logger.info("Testing on slave address " + str(self.address)) found = False - if not self.address in locks: + 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, so the port will be enough @@ -54,7 +58,8 @@ def test_connection(self): mbdev.serial.parity = minimalmodbus.serial.PARITY_NONE mbdev.serial.stopbits = serial.STOPBITS_ONE mbdev.serial.baudrate = 9600 - mbdev.serial.timeout = 0.4 # yes, 400ms is long but the BMS is sometimes really slow in responding, so this seems a good compromize + # 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): @@ -282,7 +287,8 @@ def read_soc_data(self): if warnings & (1 << 0): self.protection.voltage_cell_high = 2 - self.protection.voltage_high = 1 # we handle a single cell OV as total OV, as long as cell_high is not explicitly handled + # 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 @@ -351,10 +357,11 @@ def read_soc_data(self): 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) + # 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 @@ -364,7 +371,8 @@ def read_soc_data(self): temps = mbdev.read_register(112, 0, 3, False) most = (temps & 0xFF) - 40 balt = ((temps >> 8) & 0xFF) - 40 - # balancer temperature is not handled separately, so let's display the max of both temperatures inside the BMS as mos temperature + # 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) @@ -413,7 +421,7 @@ def read_cell_data(self): ) continue break - if result == False: + if result is False: return False if len(self.cells) != self.cell_count: From 1611f94519d7ca10d341c55ee5e1cda597f8bba7 Mon Sep 17 00:00:00 2001 From: Raphael Mack Date: Mon, 22 May 2023 23:05:04 +0200 Subject: [PATCH 10/15] lint is not my friend (yet) --- etc/dbus-serialbattery/bms/heltecmodbus.py | 7 +++++-- etc/dbus-serialbattery/utils.py | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/etc/dbus-serialbattery/bms/heltecmodbus.py b/etc/dbus-serialbattery/bms/heltecmodbus.py index 8a66ccd7..f39d1b60 100644 --- a/etc/dbus-serialbattery/bms/heltecmodbus.py +++ b/etc/dbus-serialbattery/bms/heltecmodbus.py @@ -45,7 +45,9 @@ def test_connection(self): 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, so the port will be enough + + # 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( @@ -95,7 +97,8 @@ def test_connection(self): ) continue break - if found: break + if found: + break return ( found diff --git a/etc/dbus-serialbattery/utils.py b/etc/dbus-serialbattery/utils.py index ebe2eb4c..7dff2d5f 100644 --- a/etc/dbus-serialbattery/utils.py +++ b/etc/dbus-serialbattery/utils.py @@ -309,6 +309,7 @@ def _get_list_from_config( "DEFAULT", "HELTEC_MODBUS_ADDR", lambda v: int(v) ) + # --------- Functions --------- def constrain(val, min_val, max_val): if min_val > max_val: From 88c7414649904cf9a48842db753367c52a0becf0 Mon Sep 17 00:00:00 2001 From: Raphael Mack Date: Mon, 22 May 2023 23:13:56 +0200 Subject: [PATCH 11/15] maybe now? --- etc/dbus-serialbattery/bms/heltecmodbus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/dbus-serialbattery/bms/heltecmodbus.py b/etc/dbus-serialbattery/bms/heltecmodbus.py index f39d1b60..983f7e84 100644 --- a/etc/dbus-serialbattery/bms/heltecmodbus.py +++ b/etc/dbus-serialbattery/bms/heltecmodbus.py @@ -45,7 +45,7 @@ def test_connection(self): 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. From ebcd99530ca475f5682da7603c21e1f1ee75e834 Mon Sep 17 00:00:00 2001 From: Raphael Mack Date: Mon, 22 May 2023 23:19:25 +0200 Subject: [PATCH 12/15] this was not me, but still: I want it green --- etc/dbus-serialbattery/battery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/dbus-serialbattery/battery.py b/etc/dbus-serialbattery/battery.py index 2b8c8d9d..57c8327e 100644 --- a/etc/dbus-serialbattery/battery.py +++ b/etc/dbus-serialbattery/battery.py @@ -248,7 +248,7 @@ def manage_charge_voltage_linear(self) -> None: voltageSum - penaltySum, utils.MIN_CELL_VOLTAGE * self.cell_count, ), - utils.MAX_CELL_VOLTAGE * self.cell_count + utils.MAX_CELL_VOLTAGE * self.cell_count, ), 3, ) From 3112209b1255297c9270fe0b7772b4ceebe7b24d Mon Sep 17 00:00:00 2001 From: Raphael Mack Date: Mon, 22 May 2023 23:37:43 +0200 Subject: [PATCH 13/15] fix the type/name assignment that was messed up by the rework of the address handling --- etc/dbus-serialbattery/bms/heltecmodbus.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/etc/dbus-serialbattery/bms/heltecmodbus.py b/etc/dbus-serialbattery/bms/heltecmodbus.py index 983f7e84..0fe041da 100644 --- a/etc/dbus-serialbattery/bms/heltecmodbus.py +++ b/etc/dbus-serialbattery/bms/heltecmodbus.py @@ -28,13 +28,8 @@ class HeltecModbus(Battery): def __init__(self, port, baud, address): - self.address = address super(HeltecModbus, self).__init__(port, baud, address) - if address != 0: - type = "#" + str(address) + "_" - else: - type = "" - self.type = type + "Heltec_Smart" + self.type = "Heltec_Smart" def test_connection(self): # call a function that will connect to the battery, send a command and retrieve the result. @@ -98,6 +93,7 @@ def test_connection(self): continue break if found: + self.type = "#" + str(self.address) + "_Heltec_Smart" break return ( From a783e5a80652d15e4c4826b10395a9c6b35a23e7 Mon Sep 17 00:00:00 2001 From: Raphael Mack Date: Mon, 22 May 2023 23:38:06 +0200 Subject: [PATCH 14/15] mmh, lint --- etc/dbus-serialbattery/battery.py | 1 - 1 file changed, 1 deletion(-) diff --git a/etc/dbus-serialbattery/battery.py b/etc/dbus-serialbattery/battery.py index 57c8327e..04f0258d 100644 --- a/etc/dbus-serialbattery/battery.py +++ b/etc/dbus-serialbattery/battery.py @@ -253,7 +253,6 @@ def manage_charge_voltage_linear(self) -> None: 3, ) - self.charge_mode = ( "Bulk dynamic" # + " (vS: " From 7ef0e64ace2efdb6f70ffa1357619b5b28604358 Mon Sep 17 00:00:00 2001 From: Raphael Mack Date: Tue, 23 May 2023 22:00:41 +0200 Subject: [PATCH 15/15] revers battery current in code instead of requireing the corresponding configuration option --- docs/docs/general/install.md | 2 +- etc/dbus-serialbattery/bms/heltecmodbus.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/general/install.md b/docs/docs/general/install.md index 0a0a87aa..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 → set INVERT_CURRENT_MEASUREMENT = -1 and 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. +* 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/etc/dbus-serialbattery/bms/heltecmodbus.py b/etc/dbus-serialbattery/bms/heltecmodbus.py index 0fe041da..9291fd26 100644 --- a/etc/dbus-serialbattery/bms/heltecmodbus.py +++ b/etc/dbus-serialbattery/bms/heltecmodbus.py @@ -251,7 +251,7 @@ def read_soc_data(self): ) time.sleep(SLPTIME) - self.current = ( + self.current = -( mbdev.read_long(78, 3, True, minimalmodbus.BYTEORDER_LITTLE) / 100 )