diff --git a/CHANGELOG.md b/CHANGELOG.md index 84bfb4bd..0dd60e19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,23 @@ # Changelog ## v1.0.x -* Added: Bluetooth: Show signal strenght of BMS in log by @mr-manuel +* Added: Bluetooth: Show signal strength of BMS in log by @mr-manuel * Added: Create unique identifier, if not provided from BMS by @mr-manuel * Added: Exclude a device from beeing used by the dbus-serialbattery driver by @mr-manuel * Added: Implement callback function for update by @seidler2547 * Added: JKBMS BLE - Show last five characters from the MAC address in the custom name (which is displayed in the device list) by @mr-manuel * Added: Save custom name and make it restart persistant by @mr-manuel +* Changed: Enable BMS that are disabled by default by specifying it in the config file. No more need to edit scripts by @mr-manuel +* Changed: Fix Sinowealth not loading https://github.com/Louisvdw/dbus-serialbattery/issues/702 by @mr-manuel * Changed: Fixed error in `reinstall-local.sh` script for Bluetooth installation by @mr-manuel +* Changed: Fixed that other devices are recognized as ANT BMS by @mr-manuel +* Changed: Fixed that other devices are recognized as Sinowealth BMS by @mr-manuel * Changed: Fixed typo in `config.ini` sample by @hoschult +* Changed: For BMS_TYPE now multiple BMS can be specified by @mr-manuel * Changed: Improved driver reinstall when multiple Bluetooth BMS are enabled by @mr-manuel * Changed: Improved Jkbms_Ble driver by @seidler2547 & @mr-manuel +* Changed: Improved battery error handling on connection loss by @mr-manuel +* Changed: Improved battery voltage handling in linear absorption mode by @ogurevich ## v1.0.20230531 diff --git a/etc/dbus-serialbattery/battery.py b/etc/dbus-serialbattery/battery.py index 99fe3688..0c7bef68 100644 --- a/etc/dbus-serialbattery/battery.py +++ b/etc/dbus-serialbattery/battery.py @@ -78,6 +78,9 @@ def __init__(self, port, baud, address): self.init_values() def init_values(self): + """ + Used to reset values, if battery unexpectly disconnects + """ self.voltage = None self.current = None self.capacity_remain = None @@ -137,12 +140,13 @@ def unique_identifier(self) -> str: since it can be changed by small amounts to make a battery unique. On +/- 5 Ah you can identify 11 batteries """ - return ( - "".join(filter(str.isalnum, self.hardware_version)) - + "_" - + str(self.capacity) - + "Ah" + string = ( + "".join(filter(str.isalnum, self.hardware_version)) + "_" + if self.hardware_version is not None and self.hardware_version != "" + else "" ) + string += str(self.capacity) + "Ah" + return string def connection_name(self) -> str: return "Serial " + self.port diff --git a/etc/dbus-serialbattery/bms/ant.py b/etc/dbus-serialbattery/bms/ant.py index 124f036f..b6e2d904 100644 --- a/etc/dbus-serialbattery/bms/ant.py +++ b/etc/dbus-serialbattery/bms/ant.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- -# disable ANT BMS by default as it causes other issues but can be enabled manually +# ANT BMS is disabled by default as it causes issues with other devices +# can be enabled by specifying it in the BMS_TYPE setting in the "config.ini" # https://github.com/Louisvdw/dbus-serialbattery/issues/479 from battery import Battery @@ -9,9 +10,9 @@ from struct import unpack_from -class Ant(Battery): +class ANT(Battery): def __init__(self, port, baud, address): - super(Ant, self).__init__(port, baud, address) + super(ANT, self).__init__(port, baud, address) self.type = self.BATTERYTYPE command_general = b"\xDB\xDB\x00\x00\x00\x00" @@ -30,6 +31,7 @@ def test_connection(self): result = False try: result = self.read_status_data() + result = result and self.refresh_data() except Exception as err: logger.error(f"Unexpected {err=}, {type(err)=}") result = False @@ -61,8 +63,15 @@ def read_status_data(self): voltage = unpack_from(">H", status_data, 4) self.voltage = voltage[0] * 0.1 + # check if data is in the thresholds, if not it's very likely that it's not an ANT BMS + if self.voltage < 0 and self.voltage > 100: + return False + current, self.soc = unpack_from(">lB", status_data, 70) self.current = 0.0 if current == 0 else current / -10 + # check if data is in the thresholds, if not it's very likely that it's not an ANT BMS + if self.soc < 0 or self.soc > 100 or abs(self.current) > 1000: + return False self.cell_count = unpack_from(">b", status_data, 123)[0] self.max_battery_voltage = utils.MAX_CELL_VOLTAGE * self.cell_count @@ -78,6 +87,9 @@ def read_status_data(self): capacity = unpack_from(">L", status_data, 75) self.capacity = capacity[0] / 1000000 + # check if data is in the thresholds, if not it's very likely that it's not an ANT BMS + if self.capacity < 0 or self.capacity > 1000: + return False capacity_remain = unpack_from(">L", status_data, 79) self.capacity_remain = capacity_remain[0] / 1000000 diff --git a/etc/dbus-serialbattery/bms/jkbms_ble.py b/etc/dbus-serialbattery/bms/jkbms_ble.py index 6a2e3011..9029da28 100644 --- a/etc/dbus-serialbattery/bms/jkbms_ble.py +++ b/etc/dbus-serialbattery/bms/jkbms_ble.py @@ -138,7 +138,7 @@ def refresh_data(self): logger.info( f"Jkbms_Ble: Bluetooth connection interrupted. Got no fresh data since {last_update}s." ) - # show Bluetooth signal strenght (RSSI) + # show Bluetooth signal strength (RSSI) bluetoothctl_info = os.popen( "bluetoothctl info " + self.address diff --git a/etc/dbus-serialbattery/bms/mnb.py b/etc/dbus-serialbattery/bms/mnb.py index 84365866..a9d38853 100644 --- a/etc/dbus-serialbattery/bms/mnb.py +++ b/etc/dbus-serialbattery/bms/mnb.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- -# disable MNB battery by default +# # MNB is disabled by default +# can be enabled by specifying it in the BMS_TYPE setting in the "config.ini" # https://github.com/Louisvdw/dbus-serialbattery/commit/65241cbff36feb861ff43dbbcfb2b495f14a01ce # remove duplicate MNB lines # https://github.com/Louisvdw/dbus-serialbattery/commit/23afec33c2fd87fd4d4c53516f0a25f290643c82 diff --git a/etc/dbus-serialbattery/bms/sinowealth.py b/etc/dbus-serialbattery/bms/sinowealth.py index 7a9d9fdb..6806217c 100755 --- a/etc/dbus-serialbattery/bms/sinowealth.py +++ b/etc/dbus-serialbattery/bms/sinowealth.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- -# disable Sinowealth by default as it causes other issues but can be enabled manually +# Sinowealth is disabled by default as it causes issues with other devices +# can be enabled by specifying it in the BMS_TYPE setting in the "config.ini" # https://github.com/Louisvdw/dbus-serialbattery/commit/7aab4c850a5c8d9c205efefc155fe62bb527da8e from battery import Battery, Cell @@ -44,8 +45,8 @@ def test_connection(self): result = False try: result = self.read_status_data() - result = result and self.read_remaining_capacity() - result = result and self.read_pack_config_data() + result = result and self.get_settings() + result = result and self.refresh_data() except Exception as err: logger.error(f"Unexpected {err=}, {type(err)=}") result = False @@ -64,9 +65,10 @@ def get_settings(self): self.min_battery_voltage = utils.MIN_CELL_VOLTAGE * self.cell_count self.hardware_version = "Daly/Sinowealth BMS " + str(self.cell_count) + " cells" - logger.info(self.hardware_version) + logger.debug(self.hardware_version) - self.read_capacity() + if not self.read_capacity(): + return False for c in range(self.cell_count): self.cells.append(Cell(False)) @@ -95,7 +97,7 @@ def read_status_data(self): # [1] - FAST_DSG MID_DSG SLOW_DSG DSGING CHGING DSGMOS CHGMOS self.discharge_fet = bool(status_data[1] >> 1 & int(1)) # DSGMOS self.charge_fet = bool(status_data[1] & int(1)) # CHGMOS - logger.info( + logger.debug( ">>> INFO: Discharge fet: %s, charge fet: %s", self.discharge_fet, self.charge_fet, @@ -145,8 +147,13 @@ def read_soc(self): # check if connection success if soc_data is False: return False - logger.info(">>> INFO: current SOC: %u", soc_data[1]) - self.soc = soc_data[1] + logger.debug(">>> INFO: current SOC: %u", soc_data[1]) + soc = soc_data[1] + # check if data is in the thresholds, if not it's very likely that it's not a Sinowealth BMS + if soc < 0 or soc > 100: + return False + + self.soc = soc return True def read_cycle_count(self): @@ -156,7 +163,7 @@ def read_cycle_count(self): if cycle_count is False: return False self.cycles = int(unpack_from(">H", cycle_count[:2])[0]) - logger.info(">>> INFO: current cycle count: %u", self.cycles) + logger.debug(">>> INFO: current cycle count: %u", self.cycles) return True def read_pack_voltage(self): @@ -165,7 +172,11 @@ def read_pack_voltage(self): return False pack_voltage = unpack_from(">H", pack_voltage_data[:-1]) pack_voltage = pack_voltage[0] / 1000 - logger.info(">>> INFO: current pack voltage: %f", pack_voltage) + logger.debug(">>> INFO: current pack voltage: %f", pack_voltage) + # check if data is in the thresholds, if not it's very likely that it's not a Sinowealth BMS + if pack_voltage < 0 or pack_voltage > 100: + return False + self.voltage = pack_voltage return True @@ -175,7 +186,11 @@ def read_pack_current(self): return False current = unpack_from(">i", current_data[:-1]) current = current[0] / 1000 - logger.info(">>> INFO: current pack current: %f", current) + logger.debug(">>> INFO: current pack current: %f", current) + # check if data is in the thresholds, if not it's very likely that it's not a Sinowealth BMS + if abs(current) > 1000: + return False + self.current = current return True @@ -187,7 +202,9 @@ def read_remaining_capacity(self): return False remaining_capacity = unpack_from(">i", remaining_capacity_data[:-1]) self.capacity_remain = remaining_capacity[0] / 1000 - logger.info(">>> INFO: remaining battery capacity: %f Ah", self.capacity_remain) + logger.debug( + ">>> INFO: remaining battery capacity: %f Ah", self.capacity_remain + ) return True def read_capacity(self): @@ -195,8 +212,13 @@ def read_capacity(self): if capacity_data is False: return False capacity = unpack_from(">i", capacity_data[:-1]) - logger.info(">>> INFO: Battery capacity: %f Ah", capacity[0] / 1000) - self.capacity = capacity[0] / 1000 + capacity = capacity[0] / 1000 + logger.debug(">>> INFO: Battery capacity: %f Ah", capacity) + # check if data is in the thresholds, if not it's very likely that it's not a Sinowealth BMS + if capacity < 0 or capacity > 1000: + return False + + self.capacity = capacity return True def read_pack_config_data(self): @@ -210,12 +232,12 @@ def read_pack_config_data(self): if self.cell_count < 1 or self.cell_count > 32: logger.error(">>> ERROR: No valid cell count returnd: %u", self.cell_count) return False - logger.info(">>> INFO: Number of cells: %u", self.cell_count) + logger.debug(">>> INFO: Number of cells: %u", self.cell_count) temp_sens_mask = int(~(1 << 6)) self.temp_sensors = ( 1 if (pack_config_data[1] & temp_sens_mask) else 2 ) # one means two - logger.info(">>> INFO: Number of temperatur sensors: %u", self.temp_sensors) + logger.debug(">>> INFO: Number of temperatur sensors: %u", self.temp_sensors) return True def read_cell_data(self): @@ -235,7 +257,7 @@ def read_cell_voltage(self, cell_index): cell_voltage = unpack_from(">H", cell_data[:-1]) cell_voltage = cell_voltage[0] / 1000 - logger.info(">>> INFO: Cell %u voltage: %f V", cell_index, cell_voltage) + logger.debug(">>> INFO: Cell %u voltage: %f V", cell_index, cell_voltage) return cell_voltage def read_temperature_data(self): @@ -248,7 +270,7 @@ def read_temperature_data(self): temp_ext1 = unpack_from(">H", temp_ext1_data[:-1]) self.to_temp(1, kelvin_to_celsius(temp_ext1[0] / 10)) - logger.info(">>> INFO: BMS external temperature 1: %f C", self.temp1) + logger.debug(">>> INFO: BMS external temperature 1: %f C", self.temp1) if self.temp_sensors == 2: temp_ext2_data = self.read_serial_data_sinowealth(self.command_temp_ext2) @@ -257,7 +279,7 @@ def read_temperature_data(self): temp_ext2 = unpack_from(">H", temp_ext2_data[:-1]) self.to_temp(2, kelvin_to_celsius(temp_ext2[0] / 10)) - logger.info(">>> INFO: BMS external temperature 2: %f C", self.temp2) + logger.debug(">>> INFO: BMS external temperature 2: %f C", self.temp2) # Internal temperature 1 seems to give a logical value temp_int1_data = self.read_serial_data_sinowealth(self.command_temp_int1) @@ -265,7 +287,7 @@ def read_temperature_data(self): return False temp_int1 = unpack_from(">H", temp_int1_data[:-1]) - logger.info( + logger.debug( ">>> INFO: BMS internal temperature 1: %f C", kelvin_to_celsius(temp_int1[0] / 10), ) @@ -276,7 +298,7 @@ def read_temperature_data(self): return False temp_int2 = unpack_from(">H", temp_int2_data[:-1]) - logger.info( + logger.debug( ">>> INFO: BMS internal temperature 2: %f C", kelvin_to_celsius(temp_int2[0] / 10), ) diff --git a/etc/dbus-serialbattery/config.default.ini b/etc/dbus-serialbattery/config.default.ini index 35d01f4c..96cd90f4 100644 --- a/etc/dbus-serialbattery/config.default.ini +++ b/etc/dbus-serialbattery/config.default.ini @@ -183,12 +183,11 @@ TIME_TO_SOC_INC_FROM = False ; --------- Additional settings --------- -; Specify only one BMS type to load else leave empty to try to load all available +; Specify one or more BMS types to load else leave empty to try to load all available ; -- Available BMS: ; Daly, Ecs, HeltecModbus, HLPdataBMS4S, Jkbms, Lifepower, LltJbd, Renogy, Seplos -; -- Available BMS, but disabled by default: -; https://louisvdw.github.io/dbus-serialbattery/general/install#how-to-enable-a-disabled-bms -; Ant, MNB, Sinowealth +; -- Available BMS, but disabled by default (just enter one or more below and it will be enabled): +; ANT, MNB, Sinowealth BMS_TYPE = ; Exclute this serial devices from the driver startup diff --git a/etc/dbus-serialbattery/dbus-serialbattery.py b/etc/dbus-serialbattery/dbus-serialbattery.py index 7794071d..5703e79b 100644 --- a/etc/dbus-serialbattery/dbus-serialbattery.py +++ b/etc/dbus-serialbattery/dbus-serialbattery.py @@ -5,7 +5,6 @@ from time import sleep from dbus.mainloop.glib import DBusGMainLoop -# from threading import Thread ## removed with https://github.com/Louisvdw/dbus-serialbattery/pull/582 import sys if sys.version_info.major == 2: @@ -32,9 +31,13 @@ from bms.renogy import Renogy from bms.seplos import Seplos -# from bms.ant import Ant -# from bms.mnb import MNB -# from bms.sinowealth import Sinowealth +# enabled only if explicitly set in config under "BMS_TYPE" +if "ANT" in utils.BMS_TYPE: + from bms.ant import ANT +if "MNB" in utils.BMS_TYPE: + from bms.mnb import MNB +if "Sinowealth" in utils.BMS_TYPE: + from bms.sinowealth import Sinowealth supported_bms_types = [ {"bms": Daly, "baud": 9600, "address": b"\x40"}, @@ -48,14 +51,20 @@ {"bms": Renogy, "baud": 9600, "address": b"\x30"}, {"bms": Renogy, "baud": 9600, "address": b"\xF7"}, {"bms": Seplos, "baud": 19200}, - # {"bms": Ant, "baud": 19200}, - # {"bms": MNB, "baud": 9600}, - # {"bms": Sinowealth}, ] + +# enabled only if explicitly set in config under "BMS_TYPE" +if "ANT" in utils.BMS_TYPE: + supported_bms_types.append({"bms": ANT, "baud": 19200}) +if "MNB" in utils.BMS_TYPE: + supported_bms_types.append({"bms": MNB, "baud": 9600}) +if "Sinowealth" in utils.BMS_TYPE: + supported_bms_types.append({"bms": Sinowealth, "baud": 9600}) + expected_bms_types = [ battery_type for battery_type in supported_bms_types - if battery_type["bms"].__name__ == utils.BMS_TYPE or utils.BMS_TYPE == "" + if battery_type["bms"].__name__ in utils.BMS_TYPE or len(utils.BMS_TYPE) == 0 ] logger.info("") @@ -70,13 +79,25 @@ def poll_battery(loop): def get_battery(_port) -> Union[Battery, None]: # all the different batteries the driver support and need to test for # try to establish communications with the battery 3 times, else exit - count = 3 - while count > 0: + retry = 1 + retries = 3 + while retry <= retries: + logger.info( + "-- Testing BMS: " + str(retry) + " of " + str(retries) + " rounds" + ) # create a new battery object that can read the battery and run connection test for test in expected_bms_types: # noinspection PyBroadException try: - logger.info("Testing " + test["bms"].__name__) + logger.info( + "Testing " + + test["bms"].__name__ + + ( + ' at address "' + f"\\x{bytes(test['address']).hex()}" + '"' + if "address" in test + else "" + ) + ) batteryClass = test["bms"] baud = test["baud"] battery: Battery = batteryClass( @@ -90,9 +111,19 @@ def get_battery(_port) -> Union[Battery, None]: except KeyboardInterrupt: return None except Exception: + ( + exception_type, + exception_object, + exception_traceback, + ) = sys.exc_info() + file = exception_traceback.tb_frame.f_code.co_filename + line = exception_traceback.tb_lineno + logger.error( + f"Exception occurred: {repr(exception_object)} of type {exception_type} in {file} line #{line}" + ) # Ignore any malfunction test_function() pass - count -= 1 + retry += 1 sleep(0.5) return None diff --git a/etc/dbus-serialbattery/dbushelper.py b/etc/dbus-serialbattery/dbushelper.py index 946eb4d6..849225c4 100644 --- a/etc/dbus-serialbattery/dbushelper.py +++ b/etc/dbus-serialbattery/dbushelper.py @@ -33,7 +33,7 @@ def __init__(self, battery): self.battery = battery self.instance = 1 self.settings = None - self.error_count = 0 + self.error = {"count": 0, "timestamp_first": None, "timestamp_last": None} self.block_because_disconnect = False self._dbusservice = VeDbusService( "com.victronenergy.battery." @@ -361,9 +361,10 @@ def publish_battery(self, loop): # This is called every battery.poll_interval milli second as set up per battery type to read and update the data try: # Call the battery's refresh_data function - success = self.battery.refresh_data() - if success: - self.error_count = 0 + result = self.battery.refresh_data() + if result: + # reset error variables + self.error["count"] = 0 self.battery.online = True # unblock charge/discharge, if it was blocked when battery went offline @@ -371,9 +372,18 @@ def publish_battery(self, loop): self.block_because_disconnect = False else: - self.error_count += 1 - # If the battery is offline for more than 10 polls (polled every second for most batteries) - if self.error_count >= 10: + # update error variables + if self.error["count"] == 0: + self.error["timestamp_first"] = int(time()) + self.error["timestamp_last"] = int(time()) + self.error["count"] += 1 + + time_since_first_error = ( + self.error["timestamp_last"] - self.error["timestamp_first"] + ) + + # if the battery did not update in 10 second, it's assumed to be offline + if time_since_first_error >= 10: self.battery.online = False self.battery.init_values() @@ -381,8 +391,8 @@ def publish_battery(self, loop): if utils.BLOCK_ON_DISCONNECT: self.block_because_disconnect = True - # Has it completely failed - if self.error_count >= 60: + # if the battery did not update in 60 second, it's assumed to be completely failed + if time_since_first_error >= 60: loop.quit() # This is to mannage CVCL diff --git a/etc/dbus-serialbattery/utils.py b/etc/dbus-serialbattery/utils.py index ca64c12a..36d8523a 100644 --- a/etc/dbus-serialbattery/utils.py +++ b/etc/dbus-serialbattery/utils.py @@ -37,11 +37,8 @@ def _get_list_from_config( ) -# battery types -# if not specified: baud = 9600 - # Constants - Need to dynamically get them in future -DRIVER_VERSION = "1.0.20230610dev" +DRIVER_VERSION = "1.0.20230611dev" zero_char = chr(48) degree_sign = "\N{DEGREE SIGN}" @@ -270,13 +267,12 @@ def _get_list_from_config( # --------- Additional settings --------- -# Specify only one BMS type to load else leave empty to try to load all available +# Specify one or more BMS types to load else leave empty to try to load all available # -- Available BMS: # Daly, Ecs, HeltecModbus, HLPdataBMS4S, Jkbms, Lifepower, LltJbd, Renogy, Seplos -# -- Available BMS, but disabled by default: -# https://louisvdw.github.io/dbus-serialbattery/general/install#how-to-enable-a-disabled-bms -# Ant, MNB, Sinowealth -BMS_TYPE = config["DEFAULT"]["BMS_TYPE"] +# -- Available BMS, but disabled by default (just enter one or more below and it will be enabled): +# ANT, MNB, Sinowealth +BMS_TYPE = _get_list_from_config("DEFAULT", "BMS_TYPE", lambda v: str(v)) # Exclute this serial devices from the driver startup # Example: /dev/ttyUSB2, /dev/ttyUSB4