diff --git a/CHANGELOG.md b/CHANGELOG.md index 16c5d936..4137b7a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,12 @@ * Added: Create unique identifier, if not provided from BMS by @mr-manuel * Added: Current average of the last 5 minutes by @mr-manuel * Added: Daly BMS - Auto reset SoC when changing to float (can be turned off in the config file) by @transistorgit +* Added: Daly BMS connect via CAN (experimental, some limits apply) with https://github.com/Louisvdw/dbus-serialbattery/pull/169 by @SamuelBrucksch and @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 - Automatic SOC reset with https://github.com/Louisvdw/dbus-serialbattery/pull/736 by @ArendsM * 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: JKBMS BMS connect via CAN (experimental, some limits apply) by @IrisCrimson and @mr-manuel * Added: LLT/JBD BMS - Discharge / Charge Mosfet and disable / enable balancer switching over remote console/GUI with https://github.com/Louisvdw/dbus-serialbattery/pull/761 by @idstein * Added: LLT/JBD BMS - Show balancer state in GUI under the IO page with https://github.com/Louisvdw/dbus-serialbattery/pull/763 by @idstein * Added: Load to bulk voltage every x days to reset the SoC to 100% for some BMS by @mr-manuel diff --git a/etc/dbus-serialbattery/daly_can.py b/etc/dbus-serialbattery/bms/daly_can.py similarity index 62% rename from etc/dbus-serialbattery/daly_can.py rename to etc/dbus-serialbattery/bms/daly_can.py index e19b1a7e..5b4927ef 100644 --- a/etc/dbus-serialbattery/daly_can.py +++ b/etc/dbus-serialbattery/bms/daly_can.py @@ -1,14 +1,26 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function, unicode_literals -from battery import Protection, Battery, Cell -from utils import * -from struct import * +from battery import Battery, Cell +from utils import ( + BATTERY_CAPACITY, + INVERT_CURRENT_MEASUREMENT, + logger, + MAX_BATTERY_CHARGE_CURRENT, + MAX_BATTERY_DISCHARGE_CURRENT, + MAX_CELL_VOLTAGE, + MIN_CELL_VOLTAGE, +) +from struct import unpack_from import can -class DalyCAN(Battery): +""" +https://github.com/Louisvdw/dbus-serialbattery/pull/169 +""" - def __init__(self,port,baud): - super(DalyCAN, self).__init__(port,baud) + +class Daly_Can(Battery): + def __init__(self, port, baud, address): + super(Daly_Can, self).__init__(port, baud, address) self.charger_connected = None self.load_connected = None self.cell_min_voltage = None @@ -18,7 +30,8 @@ def __init__(self,port,baud): self.poll_interval = 1000 self.poll_step = 0 self.type = self.BATTERYTYPE - self.bus = None + self.can_bus = None + # command bytes [Priority=18][Command=94][BMS ID=01][Uplink ID=40] command_base = 0x18940140 command_soc = 0x18900140 @@ -41,8 +54,8 @@ def __init__(self,port,baud): response_temp = 0x18964001 response_cell_balance = 0x18974001 response_alarm = 0x18984001 - - BATTERYTYPE = "Daly_CAN" + + BATTERYTYPE = "Daly_Can" LENGTH_CHECK = 4 LENGTH_POS = 3 CURRENT_ZERO_CONSTANT = 30000 @@ -52,62 +65,73 @@ def test_connection(self): result = False # TODO handle errors? - can_filters = [{"can_id":self.response_base, "can_mask": 0xFFFFFFF}, - {"can_id":self.response_soc, "can_mask": 0xFFFFFFF}, - {"can_id":self.response_minmax_cell_volts, "can_mask": 0xFFFFFFF}, - {"can_id":self.response_minmax_temp, "can_mask": 0xFFFFFFF}, - {"can_id":self.response_fet, "can_mask": 0xFFFFFFF}, - {"can_id":self.response_status, "can_mask": 0xFFFFFFF}, - {"can_id":self.response_cell_volts, "can_mask": 0xFFFFFFF}, - {"can_id":self.response_temp, "can_mask": 0xFFFFFFF}, - {"can_id":self.response_cell_balance, "can_mask": 0xFFFFFFF}, - {"can_id":self.response_alarm, "can_mask": 0xFFFFFFF}] - self.bus = can.Bus(interface='socketcan', - channel='can0', - receive_own_messages=False, - can_filters=can_filters) - - result = self.read_status_data(self.bus) + can_filters = [ + {"can_id": self.response_base, "can_mask": 0xFFFFFFF}, + {"can_id": self.response_soc, "can_mask": 0xFFFFFFF}, + {"can_id": self.response_minmax_cell_volts, "can_mask": 0xFFFFFFF}, + {"can_id": self.response_minmax_temp, "can_mask": 0xFFFFFFF}, + {"can_id": self.response_fet, "can_mask": 0xFFFFFFF}, + {"can_id": self.response_status, "can_mask": 0xFFFFFFF}, + {"can_id": self.response_cell_volts, "can_mask": 0xFFFFFFF}, + {"can_id": self.response_temp, "can_mask": 0xFFFFFFF}, + {"can_id": self.response_cell_balance, "can_mask": 0xFFFFFFF}, + {"can_id": self.response_alarm, "can_mask": 0xFFFFFFF}, + ] + self.can_bus = can.Bus( + interface="socketcan", + channel=self.port, + receive_own_messages=False, + can_filters=can_filters, + ) + + result = self.read_status_data(self.can_bus) return result def get_settings(self): self.capacity = BATTERY_CAPACITY - self.max_battery_current = MAX_BATTERY_CURRENT + self.max_battery_current = MAX_BATTERY_CHARGE_CURRENT self.max_battery_discharge_current = MAX_BATTERY_DISCHARGE_CURRENT return True def refresh_data(self): result = False - result = self.read_soc_data(self.bus) - result = result and self.read_fed_data(self.bus) + result = self.read_soc_data(self.can_bus) + result = result and self.read_fed_data(self.can_bus) if self.poll_step == 0: - # This must be listed in step 0 as get_min_cell_voltage and get_max_cell_voltage in battery.py needs it at first cycle for publish_dbus in dbushelper.py - result = result and self.read_cell_voltage_range_data(self.bus) + # This must be listed in step 0 as get_min_cell_voltage and get_max_cell_voltage in battery.py + # needs it at first cycle for publish_dbus in dbushelper.py + result = result and self.read_cell_voltage_range_data(self.can_bus) elif self.poll_step == 1: - result = result and self.read_alarm_data(self.bus) + result = result and self.read_alarm_data(self.can_bus) elif self.poll_step == 2: - result = result and self.read_cells_volts(self.bus) + result = result and self.read_cells_volts(self.can_bus) elif self.poll_step == 3: - result = result and self.read_temperature_range_data(self.bus) - #else: # A placeholder to remind this is the last step. Add any additional steps before here + result = result and self.read_temperature_range_data(self.can_bus) + # else: # A placeholder to remind this is the last step. Add any additional steps before here # This is last step so reset poll_step self.poll_step = -1 self.poll_step += 1 - + return result - def read_status_data(self, bus): - status_data = self.read_bus_data_daly(bus, self.command_status) + def read_status_data(self, can_bus): + status_data = self.read_bus_data_daly(can_bus, self.command_status) # check if connection success if status_data is False: logger.debug("read_status_data") return False - self.cell_count, self.temp_sensors, self.charger_connected, self.load_connected, \ - state, self.cycles = unpack_from('>bb??bhx', status_data) + ( + self.cell_count, + self.temp_sensors, + self.charger_connected, + self.load_connected, + state, + self.cycles, + ) = unpack_from(">bb??bhx", status_data) self.max_battery_voltage = MAX_CELL_VOLTAGE * self.cell_count self.min_battery_voltage = MIN_CELL_VOLTAGE * self.cell_count @@ -116,10 +140,10 @@ def read_status_data(self, bus): logger.info(self.hardware_version) return True - def read_soc_data(self, ser): + def read_soc_data(self, ser): # Ensure data received is valid crntMinValid = -(MAX_BATTERY_DISCHARGE_CURRENT * 2.1) - crntMaxValid = (MAX_BATTERY_CURRENT * 1.3) + crntMaxValid = MAX_BATTERY_CHARGE_CURRENT * 1.3 triesValid = 2 while triesValid > 0: soc_data = self.read_bus_data_daly(ser, self.command_soc) @@ -127,15 +151,19 @@ def read_soc_data(self, ser): if soc_data is False: return False - voltage, tmp, current, soc = unpack_from('>hhhh', soc_data) - current = ((current - self.CURRENT_ZERO_CONSTANT) / -10 * INVERT_CURRENT_MEASUREMENT) - #logger.info("voltage: " + str(voltage) + ", current: " + str(current) + ", soc: " + str(soc)) + voltage, tmp, current, soc = unpack_from(">hhhh", soc_data) + current = ( + (current - self.CURRENT_ZERO_CONSTANT) + / -10 + * INVERT_CURRENT_MEASUREMENT + ) + # logger.info("voltage: " + str(voltage) + ", current: " + str(current) + ", soc: " + str(soc)) if crntMinValid < current < crntMaxValid: - self.voltage = (voltage / 10) + self.voltage = voltage / 10 self.current = current - self.soc = (soc / 10) + self.soc = soc / 10 return True - + logger.warning("read_soc_data - triesValid " + str(triesValid)) triesValid -= 1 @@ -148,12 +176,20 @@ def read_alarm_data(self, ser): logger.warning("read_alarm_data") return False - al_volt, al_temp, al_crnt_soc, al_diff, \ - al_mos, al_misc1, al_misc2, al_fault = unpack_from('>bbbbbbbb', alarm_data) + ( + al_volt, + al_temp, + al_crnt_soc, + al_diff, + al_mos, + al_misc1, + al_misc2, + al_fault, + ) = unpack_from(">bbbbbbbb", alarm_data) if al_volt & 48: # High voltage levels - Alarm - self.voltage_high = 2 + self.voltage_high = 2 elif al_volt & 15: # High voltage Warning levels - Pre-alarm self.voltage_high = 1 @@ -171,7 +207,7 @@ def read_alarm_data(self, ser): if al_temp & 2: # High charge temp - Alarm - self.temp_high_charge = 2 + self.temp_high_charge = 2 elif al_temp & 1: # High charge temp - Pre-alarm self.temp_high_charge = 1 @@ -180,17 +216,16 @@ def read_alarm_data(self, ser): if al_temp & 8: # Low charge temp - Alarm - self.temp_low_charge = 2 + self.temp_low_charge = 2 elif al_temp & 4: # Low charge temp - Pre-alarm self.temp_low_charge = 1 else: self.temp_low_charge = 0 - if al_temp & 32: # High discharge temp - Alarm - self.temp_high_discharge = 2 + self.temp_high_discharge = 2 elif al_temp & 16: # High discharge temp - Pre-alarm self.temp_high_discharge = 1 @@ -199,34 +234,34 @@ def read_alarm_data(self, ser): if al_temp & 128: # Low discharge temp - Alarm - self.temp_low_discharge = 2 + self.temp_low_discharge = 2 elif al_temp & 64: # Low discharge temp - Pre-alarm self.temp_low_discharge = 1 else: self.temp_low_discharge = 0 - #if al_crnt_soc & 2: + # if al_crnt_soc & 2: # # High charge current - Alarm - # self.current_over = 2 - #elif al_crnt_soc & 1: + # self.current_over = 2 + # elif al_crnt_soc & 1: # # High charge current - Pre-alarm # self.current_over = 1 - #else: + # else: # self.current_over = 0 - #if al_crnt_soc & 8: + # if al_crnt_soc & 8: # # High discharge current - Alarm - # self.current_over = 2 - #elif al_crnt_soc & 4: + # self.current_over = 2 + # elif al_crnt_soc & 4: # # High discharge current - Pre-alarm # self.current_over = 1 - #else: + # else: # self.current_over = 0 if al_crnt_soc & 2 or al_crnt_soc & 8: # High charge/discharge current - Alarm - self.current_over = 2 + self.current_over = 2 elif al_crnt_soc & 1 or al_crnt_soc & 4: # High charge/discharge current - Pre-alarm self.current_over = 1 @@ -241,18 +276,20 @@ def read_alarm_data(self, ser): self.soc_low = 1 else: self.soc_low = 0 - + return True - def read_cells_volts(self, bus): + def read_cells_volts(self, can_bus): if self.cell_count is not None: - cells_volts_data = self.read_bus_data_daly(bus, self.command_cell_volts, 6) + cells_volts_data = self.read_bus_data_daly( + can_bus, self.command_cell_volts, 6 + ) if cells_volts_data is False: logger.warning("read_cells_volts") return False frameCell = [0, 0, 0] - lowMin = (MIN_CELL_VOLTAGE / 2) + lowMin = MIN_CELL_VOLTAGE / 2 frame = 0 bufIdx = 0 @@ -262,15 +299,19 @@ def read_cells_volts(self, bus): for idx in range(self.cell_count): self.cells.append(Cell(True)) - while bufIdx < len(cells_volts_data): - frame, frameCell[0], frameCell[1], frameCell[2] = unpack_from('>Bhhh', cells_volts_data, bufIdx) - for idx in range(3): + while bufIdx < len(cells_volts_data): + frame, frameCell[0], frameCell[1], frameCell[2] = unpack_from( + ">Bhhh", cells_volts_data, bufIdx + ) + 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 += 8 + self.cells[cellnum].voltage = ( + None if cellVoltage < lowMin else cellVoltage + ) + bufIdx += 8 return True @@ -281,7 +322,12 @@ def read_cell_voltage_range_data(self, ser): logger.warning("read_cell_voltage_range_data") return False - cell_max_voltage,self.cell_max_no,cell_min_voltage, self.cell_min_no = unpack_from('>hbhb', minmax_data) + ( + cell_max_voltage, + self.cell_max_no, + cell_min_voltage, + self.cell_min_no, + ) = unpack_from(">hbhb", minmax_data) # Daly cells numbers are 1 based and not 0 based self.cell_min_no -= 1 self.cell_max_no -= 1 @@ -297,7 +343,7 @@ def read_temperature_range_data(self, ser): logger.debug("read_temperature_range_data") return False - max_temp,max_no,min_temp, min_no = unpack_from('>bbbb', minmax_data) + max_temp, max_no, min_temp, min_no = unpack_from(">bbbb", minmax_data) self.temp1 = min_temp - self.TEMP_ZERO_CONSTANT self.temp2 = max_temp - self.TEMP_ZERO_CONSTANT return True @@ -309,24 +355,30 @@ def read_fed_data(self, ser): logger.debug("read_fed_data") return False - status, self.charge_fet, self.discharge_fet, bms_cycles, capacity_remain = unpack_from('>b??BL', fed_data) + ( + status, + self.charge_fet, + self.discharge_fet, + bms_cycles, + capacity_remain, + ) = unpack_from(">b??BL", fed_data) self.capacity_remain = capacity_remain / 1000 return True - def read_bus_data_daly(self, bus, command, expectedMessageCount = 1): + def read_bus_data_daly(self, can_bus, command, expectedMessageCount=1): # TODO handling of error cases message = can.Message(arbitration_id=command) - bus.send(message, timeout=0.2) + can_bus.send(message, timeout=0.2) response = bytearray() # TODO use async notifier instead of this where we expect a specific frame to be received - # this could end up in a deadlock if a package is not received + # this could end up in a deadlock if a package is not received count = 0 - for msg in bus: + for msg in can_bus: # print(f"{msg.arbitration_id:X}: {msg.data}") # logger.info('Frame: ' + ", ".join(hex(b) for b in msg.data)) response.extend(msg.data) count += 1 if count == expectedMessageCount: - break - return response \ No newline at end of file + break + return response diff --git a/etc/dbus-serialbattery/bms/jkbms_brn.py b/etc/dbus-serialbattery/bms/jkbms_brn.py index 1f19a881..9f3f80ed 100644 --- a/etc/dbus-serialbattery/bms/jkbms_brn.py +++ b/etc/dbus-serialbattery/bms/jkbms_brn.py @@ -291,7 +291,12 @@ def crc(self, arr: bytearray, length: int) -> int: return crc.to_bytes(2, "little")[0] async def write_register( - self, address, vals: bytearray, length: int, bleakC: BleakClient, awaitresponse: bool + self, + address, + vals: bytearray, + length: int, + bleakC: BleakClient, + awaitresponse: bool, ): frame = bytearray(20) frame[0] = 0xAA # start sequence @@ -428,7 +433,7 @@ async def enable_charging(self, c): def jk_float_to_hex_little(self, val: float): intval = int(val * 1000) - hexval = f'{intval:0>8X}' + hexval = f"{intval:0>8X}" return bytearray.fromhex(hexval)[::-1] async def reset_soc_jk(self, c): @@ -436,15 +441,31 @@ async def reset_soc_jk(self, c): # That will trigger a High Voltage Alert and resets SOC to 100% ovp_trigger = round(self.max_cell_voltage - 0.05, 3) ovpr_trigger = round(self.max_cell_voltage - 0.10, 3) - await self.write_register(JK_REGISTER_OVPR, self.jk_float_to_hex_little(ovpr_trigger), 0x04, c, True) - await self.write_register(JK_REGISTER_OVP, self.jk_float_to_hex_little(ovp_trigger), 0x04, c, True) + await self.write_register( + JK_REGISTER_OVPR, self.jk_float_to_hex_little(ovpr_trigger), 0x04, c, True + ) + await self.write_register( + JK_REGISTER_OVP, self.jk_float_to_hex_little(ovp_trigger), 0x04, c, True + ) # Give BMS some time to recognize await asyncio.sleep(5) # Set values back to initial values - await self.write_register(JK_REGISTER_OVP, self.jk_float_to_hex_little(self.ovp_initial_voltage), 0X04, c, True) - await self.write_register(JK_REGISTER_OVPR, self.jk_float_to_hex_little(self.ovpr_initial_voltage), 0x04, c, True) + await self.write_register( + JK_REGISTER_OVP, + self.jk_float_to_hex_little(self.ovp_initial_voltage), + 0x04, + c, + True, + ) + await self.write_register( + JK_REGISTER_OVPR, + self.jk_float_to_hex_little(self.ovpr_initial_voltage), + 0x04, + c, + True, + ) logging.info("JK BMS SOC reset finished.") diff --git a/etc/dbus-serialbattery/bms/jkbms_can.py b/etc/dbus-serialbattery/bms/jkbms_can.py new file mode 100644 index 00000000..2021771d --- /dev/null +++ b/etc/dbus-serialbattery/bms/jkbms_can.py @@ -0,0 +1,267 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function, unicode_literals +from battery import Battery, Cell +from utils import ( + is_bit_set, + logger, + MAX_BATTERY_CHARGE_CURRENT, + MAX_BATTERY_DISCHARGE_CURRENT, + MAX_CELL_VOLTAGE, + MIN_CELL_VOLTAGE, + zero_char, +) +from struct import unpack_from +import can +import time + +""" +https://github.com/Louisvdw/dbus-serialbattery/compare/dev...IrisCrimson:dbus-serialbattery:jkbms_can + +# Restrictions seen from code: +- +""" + + +class Jkbms_Can(Battery): + def __init__(self, port, baud, address): + super(Jkbms_Can, self).__init__(port, baud, address) + self.can_bus = False + self.cell_count = 1 + self.poll_interval = 1500 + self.type = self.BATTERYTYPE + self.last_error_time = time.time() + self.error_active = False + + def __del__(self): + if self.can_bus: + self.can_bus.shutdown() + self.can_bus = False + logger.debug("bus shutdown") + + BATTERYTYPE = "Jkbms_Can" + CAN_BUS_TYPE = "socketcan" + + CURRENT_ZERO_CONSTANT = 400 + BATT_STAT = "BATT_STAT" + CELL_VOLT = "CELL_VOLT" + CELL_TEMP = "CELL_TEMP" + ALM_INFO = "ALM_INFO" + + MESSAGES_TO_READ = 100 + + CAN_FRAMES = { + BATT_STAT: 0x02F4, + CELL_VOLT: 0x04F4, + CELL_TEMP: 0x05F4, + ALM_INFO: 0x07F4, + } + + 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 + return self.read_status_data() + + def get_settings(self): + # After successful connection get_settings will be call to set up the battery. + # Set the current limits, populate cell count, etc + # Return True if success, False for failure + self.max_battery_current = MAX_BATTERY_CHARGE_CURRENT + self.max_battery_discharge_current = MAX_BATTERY_DISCHARGE_CURRENT + self.max_battery_voltage = MAX_CELL_VOLTAGE * self.cell_count + self.min_battery_voltage = MIN_CELL_VOLTAGE * self.cell_count + + # init the cell array add only missing Cell instances + missing_instances = self.cell_count - len(self.cells) + if missing_instances > 0: + for c in range(missing_instances): + self.cells.append(Cell(False)) + + self.hardware_version = "JKBMS CAN " + str(self.cell_count) + " cells" + 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 + result = self.read_status_data() + + return result + + def read_status_data(self): + status_data = self.read_serial_data_jkbms_CAN() + # check if connection success + if status_data is False: + return False + + return True + + def to_fet_bits(self, byte_data): + tmp = bin(byte_data)[2:].rjust(2, zero_char) + self.charge_fet = is_bit_set(tmp[1]) + self.discharge_fet = is_bit_set(tmp[0]) + + def to_protection_bits(self, byte_data): + tmp = bin(byte_data | 0xFF00000000) + pos = len(tmp) + logger.debug(tmp) + self.protection.cell_overvoltage = 2 if int(tmp[pos - 2 : pos], 2) > 0 else 0 + self.protection.voltage_cell_low = ( + 2 if int(tmp[pos - 4 : pos - 2], 2) > 0 else 0 + ) + self.protection.voltage_high = 2 if int(tmp[pos - 6 : pos - 4], 4) > 0 else 0 + self.protection.voltage_low = 2 if int(tmp[pos - 8 : pos - 6], 2) > 0 else 0 + self.protection.cell_imbalance = 2 if int(tmp[pos - 10 : pos - 8], 2) > 0 else 0 + self.protection.current_under = 2 if int(tmp[pos - 12 : pos - 10], 2) > 0 else 0 + self.protection.current_over = 2 if int(tmp[pos - 14 : pos - 12], 2) > 0 else 0 + + # there is just a BMS and Battery temp alarm (not for charg and discharge) + self.protection.temp_high_charge = ( + 2 if int(tmp[pos - 16 : pos - 14], 2) > 0 else 0 + ) + self.protection.temp_high_discharge = ( + 2 if int(tmp[pos - 16 : pos - 14], 2) > 0 else 0 + ) + self.protection.temp_low_charge = ( + 2 if int(tmp[pos - 18 : pos - 16], 2) > 0 else 0 + ) + self.protection.temp_low_discharge = ( + 2 if int(tmp[pos - 18 : pos - 16], 2) > 0 else 0 + ) + self.protection.temp_high_charge = ( + 2 if int(tmp[pos - 20 : pos - 18], 2) > 0 else 0 + ) + self.protection.temp_high_discharge = ( + 2 if int(tmp[pos - 20 : pos - 18], 2) > 0 else 0 + ) + self.protection.soc_low = 2 if int(tmp[pos - 22 : pos - 20], 2) > 0 else 0 + self.protection.internal_failure = ( + 2 if int(tmp[pos - 24 : pos - 22], 2) > 0 else 0 + ) + self.protection.internal_failure = ( + 2 if int(tmp[pos - 26 : pos - 24], 2) > 0 else 0 + ) + self.protection.internal_failure = ( + 2 if int(tmp[pos - 28 : pos - 26], 2) > 0 else 0 + ) + self.protection.internal_failure = ( + 2 if int(tmp[pos - 30 : pos - 28], 2) > 0 else 0 + ) + + def reset_protection_bits(self): + self.protection.cell_overvoltage = 0 + self.protection.voltage_cell_low = 0 + self.protection.voltage_high = 0 + self.protection.voltage_low = 0 + self.protection.cell_imbalance = 0 + self.protection.current_under = 0 + self.protection.current_over = 0 + + # there is just a BMS and Battery temp alarm (not for charg and discharge) + self.protection.temp_high_charge = 0 + self.protection.temp_high_discharge = 0 + self.protection.temp_low_charge = 0 + self.protection.temp_low_discharge = 0 + self.protection.temp_high_charge = 0 + self.protection.temp_high_discharge = 0 + self.protection.soc_low = 0 + self.protection.internal_failure = 0 + self.protection.internal_failure = 0 + self.protection.internal_failure = 0 + self.protection.internal_failure = 0 + + def read_serial_data_jkbms_CAN(self): + if self.can_bus is False: + logger.debug("Can bus init") + # intit the can interface + try: + self.can_bus = can.interface.Bus( + bustype=self.CAN_BUS_TYPE, channel=self.port, bitrate=self.baud_rate + ) + except can.CanError as e: + logger.error(e) + + if self.can_bus is None: + return False + + logger.debug("Can bus init done") + + # reset errors after timeout + if ((time.time() - self.last_error_time) > 120.0) and self.error_active is True: + self.error_active = False + self.reset_protection_bits() + + # read msgs until we get one we want + messages_to_read = self.MESSAGES_TO_READ + while messages_to_read > 0: + msg = self.can_bus.recv(1) + if msg is None: + logger.info("No CAN Message received") + return False + + if msg is not None: + # print("message received") + messages_to_read -= 1 + # print(messages_to_read) + if msg.arbitration_id == self.CAN_FRAMES[self.BATT_STAT]: + voltage = unpack_from(" self.cell_count: + self.cell_count = max_cell_cnt + self.get_settings() + + for c_nr in range(len(self.cells)): + self.cells[c_nr].balance = False + + if self.cell_count == len(self.cells): + self.cells[max_cell_nr - 1].voltage = max_cell_volt + self.cells[max_cell_nr - 1].balance = True + + self.cells[min_cell_nr - 1].voltage = min_cell_volt + self.cells[min_cell_nr - 1].balance = True + + elif msg.arbitration_id == self.CAN_FRAMES[self.CELL_TEMP]: + max_temp = unpack_from(" str: if testbms.test_connection(): logger.info("Connection established to " + testbms.__class__.__name__) battery = testbms + elif port.startswith("can"): + """ + Import CAN classes only, if it's a can port, else the driver won't start due to missing python modules + This prevent problems when using the driver only with a serial connection + """ + from bms.daly_can import Daly_Can + from bms.jkbms_can import Jkbms_Can + + # only try CAN BMS on CAN port + supported_bms_types = [ + {"bms": Daly_Can, "baud": 250000}, + {"bms": Jkbms_Can, "baud": 250000}, + ] + + expected_bms_types = [ + battery_type + for battery_type in supported_bms_types + if battery_type["bms"].__name__ in utils.BMS_TYPE + or len(utils.BMS_TYPE) == 0 + ] + + battery = get_battery(port) else: battery = get_battery(port) diff --git a/etc/dbus-serialbattery/disable.sh b/etc/dbus-serialbattery/disable.sh index 13216cfe..3beacfad 100755 --- a/etc/dbus-serialbattery/disable.sh +++ b/etc/dbus-serialbattery/disable.sh @@ -16,6 +16,7 @@ pkill -f "/opt/victronenergy/serial-starter/serial-starter.sh" # remove services rm -rf /service/dbus-serialbattery.* rm -rf /service/dbus-blebattery.* +rm -rf /service/dbus-canbattery.* # kill driver, if running # serial @@ -25,7 +26,11 @@ pkill -f "python .*/dbus-serialbattery.py /dev/tty.*" # bluetooth pkill -f "supervise dbus-blebattery.*" pkill -f "multilog .* /var/log/dbus-blebattery.*" -pkill -f "python .*/dbus-serialbattery.py .*_Ble" +pkill -f "python .*/dbus-serialbattery.py .*_Ble.*" +# can +pkill -f "supervise dbus-canbattery.*" +pkill -f "multilog .* /var/log/dbus-canbattery.*" +pkill -f "python .*/dbus-serialbattery.py can.*" # remove install script from rc.local sed -i "/bash \/data\/etc\/dbus-serialbattery\/reinstall-local.sh/d" /data/rc.local diff --git a/etc/dbus-serialbattery/reinstall-local.sh b/etc/dbus-serialbattery/reinstall-local.sh index 7569fbb6..1532cf16 100755 --- a/etc/dbus-serialbattery/reinstall-local.sh +++ b/etc/dbus-serialbattery/reinstall-local.sh @@ -144,8 +144,8 @@ IFS="," read -r -a bms_array <<< "$bluetooth_bms_clean" #declare -p bms_array # readarray -td, bms_array <<< "$bluetooth_bms_clean,"; unset 'bms_array[-1]'; declare -p bms_array; -length=${#bms_array[@]} -# echo $length +bluetooth_length=${#bms_array[@]} +# echo $bluetooth_length # stop all dbus-blebattery services, if at least one exists if [ -d "/service/dbus-blebattery.0" ]; then @@ -164,10 +164,10 @@ if [ -d "/service/dbus-blebattery.0" ]; then fi -if [ "$length" -gt 0 ]; then +if [ "$bluetooth_length" -gt 0 ]; then echo - echo "Found $length Bluetooth BMS in the config file!" + echo "Found $bluetooth_length Bluetooth BMS in the config file!" echo /etc/init.d/bluetooth stop @@ -266,7 +266,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 "/service/dbus-canbattery.$1/log/run" + chmod 755 "/service/dbus-canbattery.$1/log/run" + + { + echo "#!/bin/sh" + echo "exec 2>&1" + echo "echo" + echo "python /opt/victronenergy/dbus-serialbattery/dbus-serialbattery.py $1" + } > "/service/dbus-canbattery.$1/run" + chmod 755 "/service/dbus-canbattery.$1/run" + } + + # Example + # install_canbattery_service can0 + # install_canbattery_service can9 + + for (( i=0; i Bluetooth in the remote console/GUI to prevent reconnects every minute." +echo " 2. Make sure to disable Bluetooth in \"Settings -> Bluetooth\" in the remote console/GUI to prevent reconnects every minute." echo echo " 3. Re-run \"/data/etc/dbus-serialbattery/reinstall-local.sh\", if the Bluetooth BMS were not added to the \"config.ini\" before." echo @@ -327,6 +446,16 @@ echo " ATTENTION!" echo " If you changed the default connection PIN of your BMS, then you have to pair the BMS first using OS tools like the \"bluetoothctl\"." echo " See https://wiki.debian.org/BluetoothUser#Using_bluetoothctl for more details." echo +echo "CAN battery connection: There are a few more steps to complete installation." +echo +echo " 1. Please add the CAN port to the config file \"/data/etc/dbus-serialbattery/config.ini\" by adding \"CAN_PORT\" = " +echo " Example with 1 CAN port: CAN_PORT = can0" +echo " Example with 3 CAN port: CAN_PORT = can0, can8, can9" +echo +echo " 2. Make sure to select a profile with 250 kbit/s in \"Settings -> Services -> VE.Can port -> CAN-bus profile\" in the remote console/GUI." +echo +echo " 3. Re-run \"/data/etc/dbus-serialbattery/reinstall-local.sh\", if the CAN port was not added to the \"config.ini\" before." +echo echo "CUSTOM SETTINGS: If you want to add custom settings, then check the settings you want to change in \"/data/etc/dbus-serialbattery/config.default.ini\"" echo " and add them to \"/data/etc/dbus-serialbattery/config.ini\" to persist future driver updates." echo diff --git a/etc/dbus-serialbattery/uninstall.sh b/etc/dbus-serialbattery/uninstall.sh index 94100a9d..9bec2518 100755 --- a/etc/dbus-serialbattery/uninstall.sh +++ b/etc/dbus-serialbattery/uninstall.sh @@ -19,12 +19,13 @@ rm -rf /opt/victronenergy/dbus-serialbattery # uninstall modules -read -r -p "Do you want to uninstall bleak, python3-pip and python3-modules? If you don't know just press enter. [y/N] " response +read -r -p "Do you want to uninstall bleak, python-can, python3-pip and python3-modules? If you don't know just press enter. [y/N] " response echo response=${response,,} # tolower if [[ $response =~ ^(y) ]]; then echo "Uninstalling modules..." pip3 uninstall bleak + pip3 uninstall python-can opkg remove python3-pip python3-modules echo "done." echo diff --git a/etc/dbus-serialbattery/utils.py b/etc/dbus-serialbattery/utils.py index 2bb73355..27541381 100644 --- a/etc/dbus-serialbattery/utils.py +++ b/etc/dbus-serialbattery/utils.py @@ -38,7 +38,7 @@ def _get_list_from_config( # Constants -DRIVER_VERSION = "1.0.20230905dev" +DRIVER_VERSION = "1.0.20230917dev" zero_char = chr(48) degree_sign = "\N{DEGREE SIGN}"