From fe6b89cfb2a1623bd835d60f76b5bcd9102c684d Mon Sep 17 00:00:00 2001 From: Eike Baran Date: Sat, 10 Dec 2022 23:10:26 +0000 Subject: [PATCH 01/22] first commits for jkbms ble integration --- etc/dbus-serialbattery/jkbms_ble.py | 139 ++++++++++++++++++++++++++++ etc/dbus-serialbattery/utils.py | 1 + 2 files changed, 140 insertions(+) create mode 100644 etc/dbus-serialbattery/jkbms_ble.py diff --git a/etc/dbus-serialbattery/jkbms_ble.py b/etc/dbus-serialbattery/jkbms_ble.py new file mode 100644 index 00000000..7535d114 --- /dev/null +++ b/etc/dbus-serialbattery/jkbms_ble.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +from battery import Protection, Battery, Cell +from utils import * +from struct import * +from jkbms import JkBmsBle +from bleak import BleakScanner + +class Jkbms_Ble(Battery): + BATTERYTYPE = "Jkbms BLE" + def __init__(self, port,baud, address): + super(Jkbms_Ble, self).__init__(port,baud) + self.type = self.BATTERYTYPE + self.jk = JkBmsBLE(address) + + + 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 + + # check if device with given mac is found, otherwise abort + devices = await BleakScanner.discover() + found=False + for d in devices + if d.address=self.jk.address + found=True + if not found: + return False + + #device was found, presumeably a jkbms so start scraping + jk.start_scraping() + tries = 1 + + while jk.get_status() == None and tries < 20: + time.sleep(0.5) + tries += 1 + + # load initial data, from here on get_status has valid values to be served to the dbus + status=jk.get_status() + if status == None: + return False + + if not status["device_info"]["vendor_id"].startswith("JK-") + return False + + logger.info("JK BMS found!") + return true + + 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 + + # Uncomment if BMS does not supply capacity + # self.capacity = BATTERY_CAPACITY + st=jk.get_status()["settings"] + + self.cell_count=st["cell_count"] + self.max_battery_charge_current = st["max_charge_current"] + self.max_battery_discharge_current = status["max_discharge_current"] + self.max_battery_voltage = st["cell_ovp"] * self.cell_count + self.min_battery_voltage = st["cell_uvp"] * self.cell_count + + for c in range(self.cell_count): + self.cells.append(Cell(False)) + + self.hardware_version = "JKBMS "+ jk.get_status()["device_info"]["hw_rev"]+" " + 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_soc_data() + #TODO: check for errors + status=jk.get_status() + if status == None: + return False + if time.time() - status["last_update"] > 30: + #if data not updated for more than 30s, sth is wrong, then fail + return False + + for c in range(self.cell_count): + self.cells[c].voltage=status["cell_info"]["voltages"][c] + + self.to_temp(1, status["cell_info"]["temperature_sensor_1"]) + self.to_temp(2, status["cell_info"]["temperature_sensor_2"]) + self.current=status["cell_info"]["current"] + self.voltage=status["cell_info"]["voltage"] + + self.soc=status["cell_info"]["battery_soc"] + self.cycles=status["cell_info"]["cycle_count"] + self.capacity=status["cell_info"]["nominal_capacity"] + return True + + + def read_status_data(self): + status_data = self.read_serial_data_template(self.command_status) + # check if connection success + if status_data is False: + return False + + self.cell_count, self.temp_sensors, self.charger_connected, self.load_connected, \ + state, self.cycles = unpack_from('>bb??bhx', status_data) + + self.hardware_version = "TemplateBMS " + str(self.cell_count) + " cells" + logger.info(self.hardware_version) + return True + + def read_soc_data(self): + soc_data = self.read_serial_data_template(self.command_soc) + # check if connection success + if soc_data is False: + return False + + voltage, current, soc = unpack_from('>hxxhh', soc_data) + self.voltage = voltage / 10 + self.current = current / -10 + self.soc = soc / 10 + return True + + def read_serial_data_template(self, command): + # use the read_serial_data() function to read the data and then do BMS spesific checks (crc, start bytes, etc) + data = read_serial_data(command, self.port, self.baud_rate, self.LENGTH_POS, self.LENGTH_CHECK) + if data is False: + return False + + start, flag, command_ret, length = unpack_from('BBBB', data) + checksum = sum(data[:-1]) & 0xFF + + if start == 165 and length == 8 and checksum == data[12]: + return data[4:length+4] + else: + logger.error(">>> ERROR: Incorrect Reply") + return False diff --git a/etc/dbus-serialbattery/utils.py b/etc/dbus-serialbattery/utils.py index 1a5a4a9c..1f8aed86 100644 --- a/etc/dbus-serialbattery/utils.py +++ b/etc/dbus-serialbattery/utils.py @@ -17,6 +17,7 @@ {"bms" : "Daly", "address" : b"\x40"}, {"bms" : "Daly", "address" : b"\x80"}, {"bms" : "Jkbms", "baud" : 115200}, + {"bms" : "Jkbms_Ble" "address" : "C8:47:8C:E4:54:0E"}, {"bms" : "Sinowealth"}, {"bms" : "Lifepower"}, {"bms" : "Renogy", "address": b"\x30"}, From 6a68d3c96aaaf62722b91f867f9aed01c65fc3f9 Mon Sep 17 00:00:00 2001 From: Eike Baran Date: Sun, 25 Dec 2022 20:38:47 +0000 Subject: [PATCH 02/22] implemented most warnings except undercurrent(?), imbalance --- etc/dbus-serialbattery/jkbms_ble.py | 83 +++++++++-------------------- 1 file changed, 25 insertions(+), 58 deletions(-) diff --git a/etc/dbus-serialbattery/jkbms_ble.py b/etc/dbus-serialbattery/jkbms_ble.py index 7535d114..d8b7473c 100644 --- a/etc/dbus-serialbattery/jkbms_ble.py +++ b/etc/dbus-serialbattery/jkbms_ble.py @@ -50,11 +50,7 @@ def get_settings(self): # After successful connection get_settings will be call to set up the battery. # Set the current limits, populate cell count, etc # Return True if success, False for failure - - # Uncomment if BMS does not supply capacity - # self.capacity = BATTERY_CAPACITY st=jk.get_status()["settings"] - self.cell_count=st["cell_count"] self.max_battery_charge_current = st["max_charge_current"] self.max_battery_discharge_current = status["max_discharge_current"] @@ -66,8 +62,6 @@ def get_settings(self): self.hardware_version = "JKBMS "+ jk.get_status()["device_info"]["hw_rev"]+" " + str(self.cell_count) + " cells" - - return True def refresh_data(self): @@ -77,63 +71,36 @@ def refresh_data(self): #result = self.read_soc_data() #TODO: check for errors - status=jk.get_status() + st=jk.get_status() if status == None: return False - if time.time() - status["last_update"] > 30: + if time.time() - st["last_update"] > 30: #if data not updated for more than 30s, sth is wrong, then fail return False for c in range(self.cell_count): - self.cells[c].voltage=status["cell_info"]["voltages"][c] + self.cells[c].voltage=st["cell_info"]["voltages"][c] - self.to_temp(1, status["cell_info"]["temperature_sensor_1"]) - self.to_temp(2, status["cell_info"]["temperature_sensor_2"]) - self.current=status["cell_info"]["current"] - self.voltage=status["cell_info"]["voltage"] - - self.soc=status["cell_info"]["battery_soc"] - self.cycles=status["cell_info"]["cycle_count"] - self.capacity=status["cell_info"]["nominal_capacity"] - return True - - - def read_status_data(self): - status_data = self.read_serial_data_template(self.command_status) - # check if connection success - if status_data is False: - return False - - self.cell_count, self.temp_sensors, self.charger_connected, self.load_connected, \ - state, self.cycles = unpack_from('>bb??bhx', status_data) - - self.hardware_version = "TemplateBMS " + str(self.cell_count) + " cells" - logger.info(self.hardware_version) - return True - - def read_soc_data(self): - soc_data = self.read_serial_data_template(self.command_soc) - # check if connection success - if soc_data is False: - return False - - voltage, current, soc = unpack_from('>hxxhh', soc_data) - self.voltage = voltage / 10 - self.current = current / -10 - self.soc = soc / 10 + self.to_temp(1, st["cell_info"]["temperature_sensor_1"]) + self.to_temp(2, st["cell_info"]["temperature_sensor_2"]) + self.current=st["cell_info"]["current"] + self.voltage=st["cell_info"]["voltage"] + + self.soc=st["cell_info"]["battery_soc"] + self.cycles=st["cell_info"]["cycle_count"] + self.capacity=st["cell_info"]["nominal_capacity"] + + #protection bits + #self.protection.soc_low = 2 if status["cell_info"]["battery_soc"] < 10.0 else 0 + #self.protection.cell_imbalance = 1 if status["warnings"]["cell_imbalance"] else 0 + + self.protection.voltage_high = 2 if st["warnings"]["cell_overvoltage"] else 0 + self.protection.voltage_low = 2 if st["warnings"]["cell_undervoltage"] else 0 + + self.protection.current_over = 2 if (st["warnings"]["charge_overcurrent"] or st["warnings"]["discharge_overcurrent"]) else 0 + + self.protection.set_IC_inspection = 2 if st["warnings"]["temperature_mos"] else 0 + self.protection.temp_high_charge = 2 if st["warnings"]["charge_overtemp"] else 0 + self.protection.temp_low_charge = 2 if st["warnings"]["charge_undertemp"] else 0 + self.protection.temp_high_discharge = 2 if st["warnings"]["discharge_overtemp"] else 0 return True - - def read_serial_data_template(self, command): - # use the read_serial_data() function to read the data and then do BMS spesific checks (crc, start bytes, etc) - data = read_serial_data(command, self.port, self.baud_rate, self.LENGTH_POS, self.LENGTH_CHECK) - if data is False: - return False - - start, flag, command_ret, length = unpack_from('BBBB', data) - checksum = sum(data[:-1]) & 0xFF - - if start == 165 and length == 8 and checksum == data[12]: - return data[4:length+4] - else: - logger.error(">>> ERROR: Incorrect Reply") - return False From f214263f2b1b7821f7cac08cf7b34140cc0158e1 Mon Sep 17 00:00:00 2001 From: Eike Baran Date: Sun, 25 Dec 2022 22:44:22 +0000 Subject: [PATCH 03/22] getting things together, removed most obvious mistakes; connection test is executed, further work required --- buildfiles.lst | 3 +- etc/dbus-serialbattery/dbus-serialbattery.py | 1 + etc/dbus-serialbattery/jkbms_ble.py | 34 ++++++++++++-------- etc/dbus-serialbattery/utils.py | 2 +- 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/buildfiles.lst b/buildfiles.lst index eb59db07..b1904116 100644 --- a/buildfiles.lst +++ b/buildfiles.lst @@ -24,8 +24,9 @@ etc/dbus-serialbattery/ant.py etc/dbus-serialbattery/util_max17853.py etc/dbus-serialbattery/mnb.py etc/dbus-serialbattery/jkbms.py +etc/dbus-serialbattery/jkbms_ble.py etc/dbus-serialbattery/sinowealth.py etc/dbus-serialbattery/renogy.py etc/dbus-serialbattery/revov.py etc/dbus-serialbattery/ecs.py -etc/dbus-serialbattery/lifepower.py \ No newline at end of file +etc/dbus-serialbattery/lifepower.py diff --git a/etc/dbus-serialbattery/dbus-serialbattery.py b/etc/dbus-serialbattery/dbus-serialbattery.py index 60fe5f00..614ecff3 100644 --- a/etc/dbus-serialbattery/dbus-serialbattery.py +++ b/etc/dbus-serialbattery/dbus-serialbattery.py @@ -20,6 +20,7 @@ from daly import Daly from ant import Ant from jkbms import Jkbms +from jkbms_ble import Jkbms_Ble from sinowealth import Sinowealth from renogy import Renogy from revov import Revov diff --git a/etc/dbus-serialbattery/jkbms_ble.py b/etc/dbus-serialbattery/jkbms_ble.py index d8b7473c..b791cd94 100644 --- a/etc/dbus-serialbattery/jkbms_ble.py +++ b/etc/dbus-serialbattery/jkbms_ble.py @@ -2,16 +2,19 @@ from battery import Protection, Battery, Cell from utils import * from struct import * -from jkbms import JkBmsBle +from jkbms_brn import JkBmsBle from bleak import BleakScanner +import asyncio +import time class Jkbms_Ble(Battery): BATTERYTYPE = "Jkbms BLE" def __init__(self, port,baud, address): super(Jkbms_Ble, self).__init__(port,baud) self.type = self.BATTERYTYPE - self.jk = JkBmsBLE(address) + self.jk = JkBmsBle(address) + logger.error("init of jkbmsble") def test_connection(self): # call a function that will connect to the battery, send a command and retrieve the result. @@ -19,38 +22,43 @@ def test_connection(self): # Return True if success, False for failure # check if device with given mac is found, otherwise abort - devices = await BleakScanner.discover() + + logger.error("test of jkbmsble") + loop = asyncio.get_event_loop() + t = loop.create_task(BleakScanner.discover()) + devices = loop.run_until_complete(t) + found=False - for d in devices - if d.address=self.jk.address + for d in devices: + if d.address == self.jk.address: found=True if not found: return False #device was found, presumeably a jkbms so start scraping - jk.start_scraping() + self.jk.start_scraping() tries = 1 - while jk.get_status() == None and tries < 20: + while self.jk.get_status() == None and tries < 20: time.sleep(0.5) tries += 1 # load initial data, from here on get_status has valid values to be served to the dbus - status=jk.get_status() + status=self.jk.get_status() if status == None: return False - if not status["device_info"]["vendor_id"].startswith("JK-") + if not status["device_info"]["vendor_id"].startswith("JK-"): return False logger.info("JK BMS found!") - return true + return True 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 - st=jk.get_status()["settings"] + st=self.jk.get_status()["settings"] self.cell_count=st["cell_count"] self.max_battery_charge_current = st["max_charge_current"] self.max_battery_discharge_current = status["max_discharge_current"] @@ -61,7 +69,7 @@ def get_settings(self): self.cells.append(Cell(False)) self.hardware_version = "JKBMS "+ jk.get_status()["device_info"]["hw_rev"]+" " + str(self.cell_count) + " cells" - + logger.info("BAT: "+self.hardware_version) return True def refresh_data(self): @@ -71,7 +79,7 @@ def refresh_data(self): #result = self.read_soc_data() #TODO: check for errors - st=jk.get_status() + st=self.jk.get_status() if status == None: return False if time.time() - st["last_update"] > 30: diff --git a/etc/dbus-serialbattery/utils.py b/etc/dbus-serialbattery/utils.py index 1f8aed86..b3ebb8a3 100644 --- a/etc/dbus-serialbattery/utils.py +++ b/etc/dbus-serialbattery/utils.py @@ -17,7 +17,7 @@ {"bms" : "Daly", "address" : b"\x40"}, {"bms" : "Daly", "address" : b"\x80"}, {"bms" : "Jkbms", "baud" : 115200}, - {"bms" : "Jkbms_Ble" "address" : "C8:47:8C:E4:54:0E"}, + {"bms" : "Jkbms_Ble", "address" : "C8:47:8C:E4:54:0E"}, {"bms" : "Sinowealth"}, {"bms" : "Lifepower"}, {"bms" : "Renogy", "address": b"\x30"}, From cb5f8657eea8a3109fdc611e668e376d6b7d434f Mon Sep 17 00:00:00 2001 From: Eike Baran Date: Mon, 26 Dec 2022 20:55:07 +0000 Subject: [PATCH 04/22] sync to upstream --- etc/dbus-serialbattery/jkbms_ble.py | 139 ++++++++++++++++++++++++++++ etc/dbus-serialbattery/utils.py | 1 + 2 files changed, 140 insertions(+) create mode 100644 etc/dbus-serialbattery/jkbms_ble.py diff --git a/etc/dbus-serialbattery/jkbms_ble.py b/etc/dbus-serialbattery/jkbms_ble.py new file mode 100644 index 00000000..7535d114 --- /dev/null +++ b/etc/dbus-serialbattery/jkbms_ble.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +from battery import Protection, Battery, Cell +from utils import * +from struct import * +from jkbms import JkBmsBle +from bleak import BleakScanner + +class Jkbms_Ble(Battery): + BATTERYTYPE = "Jkbms BLE" + def __init__(self, port,baud, address): + super(Jkbms_Ble, self).__init__(port,baud) + self.type = self.BATTERYTYPE + self.jk = JkBmsBLE(address) + + + 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 + + # check if device with given mac is found, otherwise abort + devices = await BleakScanner.discover() + found=False + for d in devices + if d.address=self.jk.address + found=True + if not found: + return False + + #device was found, presumeably a jkbms so start scraping + jk.start_scraping() + tries = 1 + + while jk.get_status() == None and tries < 20: + time.sleep(0.5) + tries += 1 + + # load initial data, from here on get_status has valid values to be served to the dbus + status=jk.get_status() + if status == None: + return False + + if not status["device_info"]["vendor_id"].startswith("JK-") + return False + + logger.info("JK BMS found!") + return true + + 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 + + # Uncomment if BMS does not supply capacity + # self.capacity = BATTERY_CAPACITY + st=jk.get_status()["settings"] + + self.cell_count=st["cell_count"] + self.max_battery_charge_current = st["max_charge_current"] + self.max_battery_discharge_current = status["max_discharge_current"] + self.max_battery_voltage = st["cell_ovp"] * self.cell_count + self.min_battery_voltage = st["cell_uvp"] * self.cell_count + + for c in range(self.cell_count): + self.cells.append(Cell(False)) + + self.hardware_version = "JKBMS "+ jk.get_status()["device_info"]["hw_rev"]+" " + 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_soc_data() + #TODO: check for errors + status=jk.get_status() + if status == None: + return False + if time.time() - status["last_update"] > 30: + #if data not updated for more than 30s, sth is wrong, then fail + return False + + for c in range(self.cell_count): + self.cells[c].voltage=status["cell_info"]["voltages"][c] + + self.to_temp(1, status["cell_info"]["temperature_sensor_1"]) + self.to_temp(2, status["cell_info"]["temperature_sensor_2"]) + self.current=status["cell_info"]["current"] + self.voltage=status["cell_info"]["voltage"] + + self.soc=status["cell_info"]["battery_soc"] + self.cycles=status["cell_info"]["cycle_count"] + self.capacity=status["cell_info"]["nominal_capacity"] + return True + + + def read_status_data(self): + status_data = self.read_serial_data_template(self.command_status) + # check if connection success + if status_data is False: + return False + + self.cell_count, self.temp_sensors, self.charger_connected, self.load_connected, \ + state, self.cycles = unpack_from('>bb??bhx', status_data) + + self.hardware_version = "TemplateBMS " + str(self.cell_count) + " cells" + logger.info(self.hardware_version) + return True + + def read_soc_data(self): + soc_data = self.read_serial_data_template(self.command_soc) + # check if connection success + if soc_data is False: + return False + + voltage, current, soc = unpack_from('>hxxhh', soc_data) + self.voltage = voltage / 10 + self.current = current / -10 + self.soc = soc / 10 + return True + + def read_serial_data_template(self, command): + # use the read_serial_data() function to read the data and then do BMS spesific checks (crc, start bytes, etc) + data = read_serial_data(command, self.port, self.baud_rate, self.LENGTH_POS, self.LENGTH_CHECK) + if data is False: + return False + + start, flag, command_ret, length = unpack_from('BBBB', data) + checksum = sum(data[:-1]) & 0xFF + + if start == 165 and length == 8 and checksum == data[12]: + return data[4:length+4] + else: + logger.error(">>> ERROR: Incorrect Reply") + return False diff --git a/etc/dbus-serialbattery/utils.py b/etc/dbus-serialbattery/utils.py index 8becb88b..85ae28c0 100644 --- a/etc/dbus-serialbattery/utils.py +++ b/etc/dbus-serialbattery/utils.py @@ -18,6 +18,7 @@ {"bms" : "Daly", "address" : b"\x40"}, {"bms" : "Daly", "address" : b"\x80"}, {"bms" : "Jkbms", "baud" : 115200}, + {"bms" : "Jkbms_Ble" "address" : "C8:47:8C:E4:54:0E"}, # {"bms" : "Sinowealth"}, {"bms" : "Lifepower"}, {"bms" : "Renogy", "address": b"\x30"}, From fcb8a17c5b2d1646a00ba6a82fb74141de0f4f28 Mon Sep 17 00:00:00 2001 From: Eike Baran Date: Mon, 26 Dec 2022 21:37:20 +0000 Subject: [PATCH 05/22] bugfixes, working on manual run :) --- etc/dbus-serialbattery/jkbms_ble.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/etc/dbus-serialbattery/jkbms_ble.py b/etc/dbus-serialbattery/jkbms_ble.py index b791cd94..14a50121 100644 --- a/etc/dbus-serialbattery/jkbms_ble.py +++ b/etc/dbus-serialbattery/jkbms_ble.py @@ -61,14 +61,14 @@ def get_settings(self): st=self.jk.get_status()["settings"] self.cell_count=st["cell_count"] self.max_battery_charge_current = st["max_charge_current"] - self.max_battery_discharge_current = status["max_discharge_current"] + self.max_battery_discharge_current = st["max_discharge_current"] self.max_battery_voltage = st["cell_ovp"] * self.cell_count self.min_battery_voltage = st["cell_uvp"] * self.cell_count for c in range(self.cell_count): self.cells.append(Cell(False)) - self.hardware_version = "JKBMS "+ jk.get_status()["device_info"]["hw_rev"]+" " + str(self.cell_count) + " cells" + self.hardware_version = "JKBMS "+ self.jk.get_status()["device_info"]["hw_rev"]+" " + str(self.cell_count) + " cells" logger.info("BAT: "+self.hardware_version) return True @@ -80,7 +80,7 @@ def refresh_data(self): #result = self.read_soc_data() #TODO: check for errors st=self.jk.get_status() - if status == None: + if st == None: return False if time.time() - st["last_update"] > 30: #if data not updated for more than 30s, sth is wrong, then fail @@ -92,11 +92,11 @@ def refresh_data(self): self.to_temp(1, st["cell_info"]["temperature_sensor_1"]) self.to_temp(2, st["cell_info"]["temperature_sensor_2"]) self.current=st["cell_info"]["current"] - self.voltage=st["cell_info"]["voltage"] + self.voltage=st["cell_info"]["total_voltage"] self.soc=st["cell_info"]["battery_soc"] self.cycles=st["cell_info"]["cycle_count"] - self.capacity=st["cell_info"]["nominal_capacity"] + self.capacity=st["cell_info"]["capacity_nominal"] #protection bits #self.protection.soc_low = 2 if status["cell_info"]["battery_soc"] < 10.0 else 0 @@ -107,7 +107,7 @@ def refresh_data(self): self.protection.current_over = 2 if (st["warnings"]["charge_overcurrent"] or st["warnings"]["discharge_overcurrent"]) else 0 - self.protection.set_IC_inspection = 2 if st["warnings"]["temperature_mos"] else 0 + self.protection.set_IC_inspection = 2 if st["cell_info"]["temperature_mos"] > 80 else 0 self.protection.temp_high_charge = 2 if st["warnings"]["charge_overtemp"] else 0 self.protection.temp_low_charge = 2 if st["warnings"]["charge_undertemp"] else 0 self.protection.temp_high_discharge = 2 if st["warnings"]["discharge_overtemp"] else 0 From 46c918b69a664eadcf71b9f29c8570bcdeb149a8 Mon Sep 17 00:00:00 2001 From: Eike Baran Date: Wed, 28 Dec 2022 00:04:27 +0000 Subject: [PATCH 06/22] give the bluetooth-jkbms a dummy-serialport that will fail to open, so that the bt-connection can be tested --- etc/dbus-serialbattery/jkbms_ble.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/dbus-serialbattery/jkbms_ble.py b/etc/dbus-serialbattery/jkbms_ble.py index 14a50121..e54cf924 100644 --- a/etc/dbus-serialbattery/jkbms_ble.py +++ b/etc/dbus-serialbattery/jkbms_ble.py @@ -10,7 +10,7 @@ class Jkbms_Ble(Battery): BATTERYTYPE = "Jkbms BLE" def __init__(self, port,baud, address): - super(Jkbms_Ble, self).__init__(port,baud) + super(Jkbms_Ble, self).__init__("zero",baud) self.type = self.BATTERYTYPE self.jk = JkBmsBle(address) From 57cee54f056a7a5a5504997047b5695f8d820044 Mon Sep 17 00:00:00 2001 From: Eike Baran Date: Fri, 6 Jan 2023 22:37:24 +0000 Subject: [PATCH 07/22] added installation dependencies to installlocal; added error-handling if no bt-device is available during test_connection --- buildfiles.lst | 1 + etc/dbus-serialbattery/installlocal.sh | 7 +- etc/dbus-serialbattery/jkbms_brn.py | 385 +++++++++++++++++++++++++ 3 files changed, 392 insertions(+), 1 deletion(-) create mode 100644 etc/dbus-serialbattery/jkbms_brn.py diff --git a/buildfiles.lst b/buildfiles.lst index 2b93e965..fe7809b0 100644 --- a/buildfiles.lst +++ b/buildfiles.lst @@ -25,6 +25,7 @@ etc/dbus-serialbattery/util_max17853.py etc/dbus-serialbattery/mnb.py etc/dbus-serialbattery/jkbms.py etc/dbus-serialbattery/jkbms_ble.py +etc/dbus-serialbattery/jkbms_brn.py etc/dbus-serialbattery/sinowealth.py etc/dbus-serialbattery/renogy.py etc/dbus-serialbattery/ecs.py diff --git a/etc/dbus-serialbattery/installlocal.sh b/etc/dbus-serialbattery/installlocal.sh index b389ccd2..76b1d6b5 100644 --- a/etc/dbus-serialbattery/installlocal.sh +++ b/etc/dbus-serialbattery/installlocal.sh @@ -1,3 +1,8 @@ #!/bin/sh +opkg update +opkg install python3-misc python3-pip +pip3 install bleak tar -zxf ./venus-data.tar.gz -C /data -sh /data/etc/dbus-serialbattery/reinstalllocal.sh \ No newline at end of file +sh /data/etc/dbus-serialbattery/reinstalllocal.sh +echo "make sure to disable Settings/Bluetooth in the Remote-Console to prevent reconnects every minute. In case of crash after ~12-16 hours disable raspberry pi 3 internal bluetooth via dtoverlay and use an external usb bluetooth-dongle" + diff --git a/etc/dbus-serialbattery/jkbms_brn.py b/etc/dbus-serialbattery/jkbms_brn.py new file mode 100644 index 00000000..f25a0c41 --- /dev/null +++ b/etc/dbus-serialbattery/jkbms_brn.py @@ -0,0 +1,385 @@ +import asyncio +from bleak import BleakScanner, BleakClient +import time +from logging import info, debug +import logging +from struct import unpack_from,calcsize +import threading +logging.basicConfig(level=logging.INFO) + + +# zero means parse all incoming data (every second) +CELL_INFO_REFRESH_S = 0 +DEVICE_INFO_REFRESH_S = 60*60*5 #every 5 Hours +CHAR_HANDLE="0000ffe1-0000-1000-8000-00805f9b34fb" +MODEL_NBR_UUID = "00002a24-0000-1000-8000-00805f9b34fb" + +COMMAND_CELL_INFO = 0x96; +COMMAND_DEVICE_INFO = 0x97; + +FRAME_VERSION_JK04 = 0x01 +FRAME_VERSION_JK02 = 0x02 +FRAME_VERSION_JK02_32S = 0x03 +PROTOCOL_VERSION_JK02 = 0x02 + + +protocol_version=PROTOCOL_VERSION_JK02 + + +MIN_RESPONSE_SIZE = 300; +MAX_RESPONSE_SIZE = 320; + +TRANSLATE_DEVICE_INFO = [ + [["device_info","hw_rev"],22,"8s"], + [["device_info","sw_rev"],30,"8s"], + [["device_info","uptime"],38," CELL_INFO_REFRESH_S: + self.last_cell_info=time.time() + info("processing frame with battery cell info") + if protocol_version == PROTOCOL_VERSION_JK02: + self.decode_cellinfo_jk02() + self.bms_status["last_update"]=time.time() + #overriding special values + ## power is calculated from voltage x current as register 122 contains unsigned power-value + self.decode_cellinfo_jk02() + self.bms_status["last_update"]=time.time() + #overriding special values + ## power is calculated from voltage x current as register 122 contains unsigned power-value + self.bms_status["cell_info"]["power"]=self.bms_status["cell_info"]["current"]*self.bms_status["cell_info"]["total_voltage"] + if self.waiting_for_response=="cell_info": + self.waiting_for_response="" + + elif info_type == 0x03: + + info("processing frame with device info") + if protocol_version == PROTOCOL_VERSION_JK02: + self.decode_device_info_jk02() + self.bms_status["last_update"]=time.time() + else: + return + if self.waiting_for_response=="device_info": + self.waiting_for_response="" + + def assemble_frame(self, data:bytearray): + #global frame_buffer + if len(self.frame_buffer) > MAX_RESPONSE_SIZE: + info("data dropped because it alone was longer than max frame length") + self.frame_buffer=[] + + if data[0] == 0x55 and data[1] == 0xAA and data[2] == 0xEB and data[3] == 0x90: + #beginning of new frame, clear buffer + self.frame_buffer=[] + + self.frame_buffer.extend(data) + + if len(self.frame_buffer) >= MIN_RESPONSE_SIZE: + #check crc; always at position 300, independent of actual frame-lentgh, so crc up to 299 + ccrc=self.crc(self.frame_buffer,300-1) + rcrc=self.frame_buffer[300-1] + debug(f"compair recvd. crc: {rcrc} vs calc. crc: {ccrc}") + if ccrc == rcrc: + debug("great success! frame complete and sane, lets go decoding") + self.decode() + self.frame_buffer=[] + + def ncallback(self, sender: int, data: bytearray): + debug(f"------> NEW PACKAGE!laenge: {len(data)}") + self.assemble_frame(data) + + def crc(self, arr:bytearray, length: int) -> int: + crc = 0; + for a in arr[:length]: + crc = crc + a; + return crc.to_bytes(2, 'little')[0] + + async def write_register(self, address, vals :bytearray,length:int, bleakC:BleakClient): + frame = bytearray(20) + frame[0] = 0xAA #start sequence + frame[1] = 0x55 #start sequence + frame[2] = 0x90 #start sequence + frame[3] = 0xEB #start sequence + frame[4] = address #holding register + frame[5] = length #size of the value in byte + frame[6] = vals[0] + frame[7] = vals[1] + frame[8] = vals[2] + frame[9] = vals[3] + frame[10] = 0x00 + frame[11] = 0x00 + frame[12] = 0x00 + frame[13] = 0x00 + frame[14] = 0x00 + frame[15] = 0x00 + frame[16] = 0x00 + frame[17] = 0x00 + frame[18] = 0x00 + + frame[19] = self.crc(frame,len(frame)-1) + debug( "Write register: ",frame ) + await bleakC.write_gatt_char(CHAR_HANDLE,frame, False) + + + + async def request_bt(self, rtype :str, client): + #global waiting_for_response + + + timeout = time.time() + + while self.waiting_for_response!="" and time.time()-timeout < 10 : + await asyncio.sleep(1) + print(self.waiting_for_response) + + + if rtype=="cell_info": + cmd=COMMAND_CELL_INFO + self.waiting_for_response="cell_info" + elif rtype=="device_info": + cmd=COMMAND_DEVICE_INFO + self.waiting_for_response="device_info" + else: + return + + await self.write_register(cmd,b'\0\0\0\0',0x00,client) + + def get_status(self): + if "settings" in self.bms_status and "cell_info" in self.bms_status: + return self.bms_status + else: + return None + + def connect_and_scrape(self): + asyncio.run(self.asy_connect_and_scrape()) + + async def asy_connect_and_scrape(self): + print("connect and scrape on address: "+self.address) + self.run = True + while self.run and self.main_thread.is_alive(): #autoreconnect + client = BleakClient(self.address) + print("btloop") + try: + await client.connect() + self.bms_status["model_nbr"]= (await client.read_gatt_char(MODEL_NBR_UUID)).decode("utf-8") + + await client.start_notify(CHAR_HANDLE, self.ncallback) + await self.request_bt("device_info", client) + + await self.request_bt("cell_info", client) + # await self.enable_charging(client) + last_dev_info=time.time() + while client.is_connected and self.run and self.main_thread.is_alive(): + if time.time()-last_dev_info>DEVICE_INFO_REFRESH_S: + last_dev_info=time.time() + await self.request_bt("device_info", client) + await asyncio.sleep(0.01) + except Exception as e: + info("error while connecting to bt: " + str(e)) + self.run=False + finally: + await client.disconnect() + + print("Exiting bt-loop") + + + def start_scraping(self): + self.main_thread=threading.current_thread() + if self.is_running(): + return + self.bt_thread.start() + info("scraping thread started -> main thread id: "+str(self.main_thread.ident)+" scraping thread: "+str(self.bt_thread.ident)) + + def stop_scraping(self): + self.run=False + while is_running(): + time.sleep(0.1) + + def is_running(self): + return self.bt_thread.is_alive() + + async def enable_charging(self,c): + # these are the registers for the control-buttons; data is 01 00 00 00 for on 00 00 00 00 for off; the following bytes up to 19 are unclear and changing dynamically -> auth-mechanism? + await self.write_register(0x1d,b'\x01\x00\x00\x00',4,c) + await self.write_register(0x1e,b'\x01\x00\x00\x00',4,c) + await self.write_register(0x1f,b'\x01\x00\x00\x00',4,c) + await self.write_register(0x40,b'\x01\x00\x00\x00',4,c) + +if __name__ == "__main__": + jk = JkBmsBle("C8:47:8C:E4:54:0E") + info("sss") + jk.start_scraping() + while True: + print("asdf") + print(jk.get_status()) + time.sleep(5) From 4555bb00f7c955dbac83a3c65cd4bfa31b6c07da Mon Sep 17 00:00:00 2001 From: Eike Baran Date: Sat, 7 Jan 2023 16:07:37 +0000 Subject: [PATCH 08/22] tidy up code --- etc/dbus-serialbattery/jkbms_brn.py | 406 +++++++++++++--------------- 1 file changed, 191 insertions(+), 215 deletions(-) diff --git a/etc/dbus-serialbattery/jkbms_brn.py b/etc/dbus-serialbattery/jkbms_brn.py index f25a0c41..36fbf483 100644 --- a/etc/dbus-serialbattery/jkbms_brn.py +++ b/etc/dbus-serialbattery/jkbms_brn.py @@ -3,19 +3,19 @@ import time from logging import info, debug import logging -from struct import unpack_from,calcsize +from struct import unpack_from, calcsize import threading logging.basicConfig(level=logging.INFO) # zero means parse all incoming data (every second) CELL_INFO_REFRESH_S = 0 -DEVICE_INFO_REFRESH_S = 60*60*5 #every 5 Hours -CHAR_HANDLE="0000ffe1-0000-1000-8000-00805f9b34fb" +DEVICE_INFO_REFRESH_S = 60*60*5 # every 5 Hours +CHAR_HANDLE = "0000ffe1-0000-1000-8000-00805f9b34fb" MODEL_NBR_UUID = "00002a24-0000-1000-8000-00805f9b34fb" -COMMAND_CELL_INFO = 0x96; -COMMAND_DEVICE_INFO = 0x97; +COMMAND_CELL_INFO = 0x96 +COMMAND_DEVICE_INFO = 0x97 FRAME_VERSION_JK04 = 0x01 FRAME_VERSION_JK02 = 0x02 @@ -23,84 +23,73 @@ PROTOCOL_VERSION_JK02 = 0x02 -protocol_version=PROTOCOL_VERSION_JK02 +protocol_version = PROTOCOL_VERSION_JK02 -MIN_RESPONSE_SIZE = 300; -MAX_RESPONSE_SIZE = 320; +MIN_RESPONSE_SIZE = 300 +MAX_RESPONSE_SIZE = 320 TRANSLATE_DEVICE_INFO = [ - [["device_info","hw_rev"],22,"8s"], - [["device_info","sw_rev"],30,"8s"], - [["device_info","uptime"],38," CELL_INFO_REFRESH_S: - self.last_cell_info=time.time() + if (CELL_INFO_REFRESH_S == 0 or + time.time() - self.last_cell_info > CELL_INFO_REFRESH_S): + self.last_cell_info = time.time() info("processing frame with battery cell info") if protocol_version == PROTOCOL_VERSION_JK02: self.decode_cellinfo_jk02() - self.bms_status["last_update"]=time.time() - #overriding special values - ## power is calculated from voltage x current as register 122 contains unsigned power-value - self.decode_cellinfo_jk02() - self.bms_status["last_update"]=time.time() - #overriding special values - ## power is calculated from voltage x current as register 122 contains unsigned power-value - self.bms_status["cell_info"]["power"]=self.bms_status["cell_info"]["current"]*self.bms_status["cell_info"]["total_voltage"] - if self.waiting_for_response=="cell_info": - self.waiting_for_response="" + self.bms_status["last_update"] = time.time() + # power is calculated from voltage x current as + # register 122 contains unsigned power-value + self.bms_status["cell_info"]["power"] = ( + self.bms_status["cell_info"]["current"] * + self.bms_status["cell_info"]["total_voltage"]) + if self.waiting_for_response == "cell_info": + self.waiting_for_response = "" elif info_type == 0x03: - info("processing frame with device info") if protocol_version == PROTOCOL_VERSION_JK02: self.decode_device_info_jk02() - self.bms_status["last_update"]=time.time() + self.bms_status["last_update"] = time.time() else: return - if self.waiting_for_response=="device_info": - self.waiting_for_response="" + if self.waiting_for_response == "device_info": + self.waiting_for_response = "" - def assemble_frame(self, data:bytearray): - #global frame_buffer + def assemble_frame(self, data: bytearray): if len(self.frame_buffer) > MAX_RESPONSE_SIZE: info("data dropped because it alone was longer than max frame length") - self.frame_buffer=[] - - if data[0] == 0x55 and data[1] == 0xAA and data[2] == 0xEB and data[3] == 0x90: - #beginning of new frame, clear buffer - self.frame_buffer=[] - + self.frame_buffer = [] + + if (data[0] == 0x55 and data[1] == 0xAA and + data[2] == 0xEB and data[3] == 0x90): + # beginning of new frame, clear buffer + self.frame_buffer = [] + self.frame_buffer.extend(data) - + if len(self.frame_buffer) >= MIN_RESPONSE_SIZE: - #check crc; always at position 300, independent of actual frame-lentgh, so crc up to 299 - ccrc=self.crc(self.frame_buffer,300-1) - rcrc=self.frame_buffer[300-1] - debug(f"compair recvd. crc: {rcrc} vs calc. crc: {ccrc}") + # check crc; always at position 300, independent of + # actual frame-lentgh, so crc up to 299 + ccrc = self.crc(self.frame_buffer, 300 - 1) + rcrc = self.frame_buffer[300-1] + debug(f"compair recvd. crc: {rcrc} vs calc. crc: {ccrc}") if ccrc == rcrc: - debug("great success! frame complete and sane, lets go decoding") + debug("great success! frame complete and sane, lets decode") self.decode() - self.frame_buffer=[] + self.frame_buffer = [] def ncallback(self, sender: int, data: bytearray): debug(f"------> NEW PACKAGE!laenge: {len(data)}") self.assemble_frame(data) - def crc(self, arr:bytearray, length: int) -> int: - crc = 0; + def crc(self, arr: bytearray, length: int) -> int: + crc = 0 for a in arr[:length]: - crc = crc + a; + crc = crc + a return crc.to_bytes(2, 'little')[0] - async def write_register(self, address, vals :bytearray,length:int, bleakC:BleakClient): + async def write_register(self, address, vals: bytearray, + length: int, bleakC: BleakClient): frame = bytearray(20) - frame[0] = 0xAA #start sequence - frame[1] = 0x55 #start sequence - frame[2] = 0x90 #start sequence - frame[3] = 0xEB #start sequence - frame[4] = address #holding register - frame[5] = length #size of the value in byte + frame[0] = 0xAA # start sequence + frame[1] = 0x55 # start sequence + frame[2] = 0x90 # start sequence + frame[3] = 0xEB # start sequence + frame[4] = address # holding register + frame[5] = length # size of the value in byte frame[6] = vals[0] frame[7] = vals[1] frame[8] = vals[2] @@ -285,98 +260,99 @@ async def write_register(self, address, vals :bytearray,length:int, bleakC:Bleak frame[16] = 0x00 frame[17] = 0x00 frame[18] = 0x00 - - frame[19] = self.crc(frame,len(frame)-1) - debug( "Write register: ",frame ) - await bleakC.write_gatt_char(CHAR_HANDLE,frame, False) - - - - async def request_bt(self, rtype :str, client): - #global waiting_for_response - + frame[19] = self.crc(frame, len(frame)-1) + debug("Write register: ", frame) + await bleakC.write_gatt_char(CHAR_HANDLE, frame, False) + async def request_bt(self, rtype: str, client): timeout = time.time() - while self.waiting_for_response!="" and time.time()-timeout < 10 : + while self.waiting_for_response != "" and time.time()-timeout < 10: await asyncio.sleep(1) print(self.waiting_for_response) - - if rtype=="cell_info": - cmd=COMMAND_CELL_INFO - self.waiting_for_response="cell_info" - elif rtype=="device_info": - cmd=COMMAND_DEVICE_INFO - self.waiting_for_response="device_info" + if rtype == "cell_info": + cmd = COMMAND_CELL_INFO + self.waiting_for_response = "cell_info" + elif rtype == "device_info": + cmd = COMMAND_DEVICE_INFO + self.waiting_for_response = "device_info" else: return - - await self.write_register(cmd,b'\0\0\0\0',0x00,client) - + + await self.write_register(cmd, b'\0\0\0\0', 0x00, client) + def get_status(self): - if "settings" in self.bms_status and "cell_info" in self.bms_status: + if "settings" in self.bms_status and "cell_info" in self.bms_status: return self.bms_status else: return None def connect_and_scrape(self): asyncio.run(self.asy_connect_and_scrape()) - + async def asy_connect_and_scrape(self): - print("connect and scrape on address: "+self.address) + print("connect and scrape on address: " + self.address) self.run = True - while self.run and self.main_thread.is_alive(): #autoreconnect + while self.run and self.main_thread.is_alive(): # autoreconnect client = BleakClient(self.address) print("btloop") try: await client.connect() - self.bms_status["model_nbr"]= (await client.read_gatt_char(MODEL_NBR_UUID)).decode("utf-8") - + self.bms_status["model_nbr"] = ( + await client.read_gatt_char(MODEL_NBR_UUID)).decode("utf-8") + await client.start_notify(CHAR_HANDLE, self.ncallback) await self.request_bt("device_info", client) - + await self.request_bt("cell_info", client) - # await self.enable_charging(client) - last_dev_info=time.time() - while client.is_connected and self.run and self.main_thread.is_alive(): - if time.time()-last_dev_info>DEVICE_INFO_REFRESH_S: - last_dev_info=time.time() + # await self.enable_charging(client) + last_dev_info = time.time() + while (client.is_connected and + self.run and self.main_thread.is_alive()): + if time.time() - last_dev_info > DEVICE_INFO_REFRESH_S: + last_dev_info = time.time() await self.request_bt("device_info", client) await asyncio.sleep(0.01) except Exception as e: info("error while connecting to bt: " + str(e)) - self.run=False + self.run = False finally: await client.disconnect() - + print("Exiting bt-loop") - - def start_scraping(self): - self.main_thread=threading.current_thread() + def start_scraping(self): + self.main_thread = threading.current_thread() if self.is_running(): return self.bt_thread.start() - info("scraping thread started -> main thread id: "+str(self.main_thread.ident)+" scraping thread: "+str(self.bt_thread.ident)) + info("scraping thread started -> main thread id: " + + str(self.main_thread.ident) + + " scraping thread: " + + str(self.bt_thread.ident)) def stop_scraping(self): - self.run=False - while is_running(): + self.run = False + while self.is_running(): time.sleep(0.1) - + def is_running(self): return self.bt_thread.is_alive() - async def enable_charging(self,c): - # these are the registers for the control-buttons; data is 01 00 00 00 for on 00 00 00 00 for off; the following bytes up to 19 are unclear and changing dynamically -> auth-mechanism? - await self.write_register(0x1d,b'\x01\x00\x00\x00',4,c) - await self.write_register(0x1e,b'\x01\x00\x00\x00',4,c) - await self.write_register(0x1f,b'\x01\x00\x00\x00',4,c) - await self.write_register(0x40,b'\x01\x00\x00\x00',4,c) + async def enable_charging(self, c): + # these are the registers for the control-buttons: + # data is 01 00 00 00 for on 00 00 00 00 for off; + # the following bytes up to 19 are unclear and changing + # dynamically -> auth-mechanism? + await self.write_register(0x1d, b'\x01\x00\x00\x00', 4, c) + await self.write_register(0x1e, b'\x01\x00\x00\x00', 4, c) + await self.write_register(0x1f, b'\x01\x00\x00\x00', 4, c) + await self.write_register(0x40, b'\x01\x00\x00\x00', 4, c) + if __name__ == "__main__": - jk = JkBmsBle("C8:47:8C:E4:54:0E") + jk = JkBmsBle("C8:47:8C:E4:54:0E") info("sss") jk.start_scraping() while True: From 68b26a348b356f7300cde25cdb28e9fdf69ef004 Mon Sep 17 00:00:00 2001 From: Eike Baran Date: Sat, 7 Jan 2023 16:31:35 +0000 Subject: [PATCH 09/22] satisfy lint --- etc/dbus-serialbattery/jkbms_brn.py | 107 +++++++++++++++------------- 1 file changed, 59 insertions(+), 48 deletions(-) diff --git a/etc/dbus-serialbattery/jkbms_brn.py b/etc/dbus-serialbattery/jkbms_brn.py index 36fbf483..24aa6b97 100644 --- a/etc/dbus-serialbattery/jkbms_brn.py +++ b/etc/dbus-serialbattery/jkbms_brn.py @@ -5,6 +5,7 @@ import logging from struct import unpack_from, calcsize import threading + logging.basicConfig(level=logging.INFO) @@ -30,12 +31,11 @@ MAX_RESPONSE_SIZE = 320 TRANSLATE_DEVICE_INFO = [ - [["device_info", "hw_rev"], 22, "8s"], - [["device_info", "sw_rev"], 30, "8s"], - [["device_info", "uptime"], 38, " CELL_INFO_REFRESH_S): + if ( + CELL_INFO_REFRESH_S == 0 or + time.time() - self.last_cell_info > CELL_INFO_REFRESH_S + ): self.last_cell_info = time.time() info("processing frame with battery cell info") if protocol_version == PROTOCOL_VERSION_JK02: @@ -190,8 +199,9 @@ def decode(self): # power is calculated from voltage x current as # register 122 contains unsigned power-value self.bms_status["cell_info"]["power"] = ( - self.bms_status["cell_info"]["current"] * - self.bms_status["cell_info"]["total_voltage"]) + self.bms_status["cell_info"]["current"] + * self.bms_status["cell_info"]["total_voltage"] + ) if self.waiting_for_response == "cell_info": self.waiting_for_response = "" @@ -210,8 +220,7 @@ def assemble_frame(self, data: bytearray): info("data dropped because it alone was longer than max frame length") self.frame_buffer = [] - if (data[0] == 0x55 and data[1] == 0xAA and - data[2] == 0xEB and data[3] == 0x90): + if data[0] == 0x55 and data[1] == 0xAA and data[2] == 0xEB and data[3] == 0x90: # beginning of new frame, clear buffer self.frame_buffer = [] @@ -221,7 +230,7 @@ def assemble_frame(self, data: bytearray): # check crc; always at position 300, independent of # actual frame-lentgh, so crc up to 299 ccrc = self.crc(self.frame_buffer, 300 - 1) - rcrc = self.frame_buffer[300-1] + rcrc = self.frame_buffer[300 - 1] debug(f"compair recvd. crc: {rcrc} vs calc. crc: {ccrc}") if ccrc == rcrc: debug("great success! frame complete and sane, lets decode") @@ -236,17 +245,18 @@ def crc(self, arr: bytearray, length: int) -> int: crc = 0 for a in arr[:length]: crc = crc + a - return crc.to_bytes(2, 'little')[0] + return crc.to_bytes(2, "little")[0] - async def write_register(self, address, vals: bytearray, - length: int, bleakC: BleakClient): + async def write_register( + self, address, vals: bytearray, length: int, bleakC: BleakClient + ): frame = bytearray(20) - frame[0] = 0xAA # start sequence - frame[1] = 0x55 # start sequence - frame[2] = 0x90 # start sequence - frame[3] = 0xEB # start sequence - frame[4] = address # holding register - frame[5] = length # size of the value in byte + frame[0] = 0xAA # start sequence + frame[1] = 0x55 # start sequence + frame[2] = 0x90 # start sequence + frame[3] = 0xEB # start sequence + frame[4] = address # holding register + frame[5] = length # size of the value in byte frame[6] = vals[0] frame[7] = vals[1] frame[8] = vals[2] @@ -260,7 +270,7 @@ async def write_register(self, address, vals: bytearray, frame[16] = 0x00 frame[17] = 0x00 frame[18] = 0x00 - frame[19] = self.crc(frame, len(frame)-1) + frame[19] = self.crc(frame, len(frame) - 1) debug("Write register: ", frame) await bleakC.write_gatt_char(CHAR_HANDLE, frame, False) @@ -280,7 +290,7 @@ async def request_bt(self, rtype: str, client): else: return - await self.write_register(cmd, b'\0\0\0\0', 0x00, client) + await self.write_register(cmd, b"\0\0\0\0", 0x00, client) def get_status(self): if "settings" in self.bms_status and "cell_info" in self.bms_status: @@ -300,7 +310,8 @@ async def asy_connect_and_scrape(self): try: await client.connect() self.bms_status["model_nbr"] = ( - await client.read_gatt_char(MODEL_NBR_UUID)).decode("utf-8") + await client.read_gatt_char(MODEL_NBR_UUID) + ).decode("utf-8") await client.start_notify(CHAR_HANDLE, self.ncallback) await self.request_bt("device_info", client) @@ -308,8 +319,7 @@ async def asy_connect_and_scrape(self): await self.request_bt("cell_info", client) # await self.enable_charging(client) last_dev_info = time.time() - while (client.is_connected and - self.run and self.main_thread.is_alive()): + while client.is_connected and self.run and self.main_thread.is_alive(): if time.time() - last_dev_info > DEVICE_INFO_REFRESH_S: last_dev_info = time.time() await self.request_bt("device_info", client) @@ -327,10 +337,11 @@ def start_scraping(self): if self.is_running(): return self.bt_thread.start() - info("scraping thread started -> main thread id: " + - str(self.main_thread.ident) + - " scraping thread: " + - str(self.bt_thread.ident)) + info("scraping thread started -> main thread id: " + + str(self.main_thread.ident) + + " scraping thread: " + + str(self.bt_thread.ident) + ) def stop_scraping(self): self.run = False @@ -345,10 +356,10 @@ async def enable_charging(self, c): # data is 01 00 00 00 for on 00 00 00 00 for off; # the following bytes up to 19 are unclear and changing # dynamically -> auth-mechanism? - await self.write_register(0x1d, b'\x01\x00\x00\x00', 4, c) - await self.write_register(0x1e, b'\x01\x00\x00\x00', 4, c) - await self.write_register(0x1f, b'\x01\x00\x00\x00', 4, c) - await self.write_register(0x40, b'\x01\x00\x00\x00', 4, c) + await self.write_register(0x1D, b"\x01\x00\x00\x00", 4, c) + await self.write_register(0x1E, b"\x01\x00\x00\x00", 4, c) + await self.write_register(0x1F, b"\x01\x00\x00\x00", 4, c) + await self.write_register(0x40, b"\x01\x00\x00\x00", 4, c) if __name__ == "__main__": From 1f9eb87e2b5b77c003d360bc6ffa23ef69931aba Mon Sep 17 00:00:00 2001 From: Eike Baran Date: Sat, 7 Jan 2023 16:45:12 +0000 Subject: [PATCH 10/22] lint #2 --- etc/dbus-serialbattery/jkbms_ble.py | 87 ++++++++++++++++++----------- 1 file changed, 54 insertions(+), 33 deletions(-) diff --git a/etc/dbus-serialbattery/jkbms_ble.py b/etc/dbus-serialbattery/jkbms_ble.py index e54cf924..a5743e59 100644 --- a/etc/dbus-serialbattery/jkbms_ble.py +++ b/etc/dbus-serialbattery/jkbms_ble.py @@ -4,13 +4,14 @@ from struct import * from jkbms_brn import JkBmsBle from bleak import BleakScanner -import asyncio +import asyncio import time + class Jkbms_Ble(Battery): BATTERYTYPE = "Jkbms BLE" - def __init__(self, port,baud, address): - super(Jkbms_Ble, self).__init__("zero",baud) + def __init__(self, port, baud, address): + super(Jkbms_Ble, self).__init__("zero", baud) self.type = self.BATTERYTYPE self.jk = JkBmsBle(address) @@ -20,22 +21,26 @@ 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 - + # check if device with given mac is found, otherwise abort - logger.error("test of jkbmsble") - loop = asyncio.get_event_loop() - t = loop.create_task(BleakScanner.discover()) - devices = loop.run_until_complete(t) + logger.info("test of jkbmsble") + try: + loop = asyncio.get_event_loop() + t = loop.create_task(BleakScanner.discover()) + devices = loop.run_until_complete(t) + except BleakErrori as e: + logger.error(str(e)) + return False - found=False + found = False for d in devices: if d.address == self.jk.address: - found=True + found = True if not found: return False - #device was found, presumeably a jkbms so start scraping + # device was found, presumeably a jkbms so start scraping self.jk.start_scraping() tries = 1 @@ -44,13 +49,13 @@ def test_connection(self): tries += 1 # load initial data, from here on get_status has valid values to be served to the dbus - status=self.jk.get_status() + status = self.jk.get_status() if status == None: return False - + if not status["device_info"]["vendor_id"].startswith("JK-"): return False - + logger.info("JK BMS found!") return True @@ -58,8 +63,8 @@ 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 - st=self.jk.get_status()["settings"] - self.cell_count=st["cell_count"] + st = self.jk.get_status()["settings"] + self.cell_count = st["cell_count"] self.max_battery_charge_current = st["max_charge_current"] self.max_battery_discharge_current = st["max_discharge_current"] self.max_battery_voltage = st["cell_ovp"] * self.cell_count @@ -68,8 +73,14 @@ def get_settings(self): for c in range(self.cell_count): self.cells.append(Cell(False)) - self.hardware_version = "JKBMS "+ self.jk.get_status()["device_info"]["hw_rev"]+" " + str(self.cell_count) + " cells" - logger.info("BAT: "+self.hardware_version) + self.hardware_version = ( + "JKBMS " + + self.jk.get_status()["device_info"]["hw_rev"] + + " " + + str(self.cell_count) + + " cells" + ) + logger.info("BAT: " + self.hardware_version) return True def refresh_data(self): @@ -77,26 +88,26 @@ def refresh_data(self): # This will be called for every iteration (1 second) # Return True if success, False for failure - #result = self.read_soc_data() - #TODO: check for errors - st=self.jk.get_status() + # result = self.read_soc_data() + # TODO: check for errors + st = self.jk.get_status() if st == None: return False if time.time() - st["last_update"] > 30: - #if data not updated for more than 30s, sth is wrong, then fail + # if data not updated for more than 30s, sth is wrong, then fail return False for c in range(self.cell_count): - self.cells[c].voltage=st["cell_info"]["voltages"][c] - + self.cells[c].voltage = st["cell_info"]["voltages"][c] + self.to_temp(1, st["cell_info"]["temperature_sensor_1"]) self.to_temp(2, st["cell_info"]["temperature_sensor_2"]) - self.current=st["cell_info"]["current"] - self.voltage=st["cell_info"]["total_voltage"] + self.current = st["cell_info"]["current"] + self.voltage = st["cell_info"]["total_voltage"] - self.soc=st["cell_info"]["battery_soc"] - self.cycles=st["cell_info"]["cycle_count"] - self.capacity=st["cell_info"]["capacity_nominal"] + self.soc = st["cell_info"]["battery_soc"] + self.cycles = st["cell_info"]["cycle_count"] + self.capacity = st["cell_info"]["capacity_nominal"] #protection bits #self.protection.soc_low = 2 if status["cell_info"]["battery_soc"] < 10.0 else 0 @@ -105,10 +116,20 @@ def refresh_data(self): self.protection.voltage_high = 2 if st["warnings"]["cell_overvoltage"] else 0 self.protection.voltage_low = 2 if st["warnings"]["cell_undervoltage"] else 0 - self.protection.current_over = 2 if (st["warnings"]["charge_overcurrent"] or st["warnings"]["discharge_overcurrent"]) else 0 - - self.protection.set_IC_inspection = 2 if st["cell_info"]["temperature_mos"] > 80 else 0 + self.protection.current_over = ( + 2 + if ( + st["warnings"]["charge_overcurrent"] + or st["warnings"]["discharge_overcurrent"] + ) + else 0 + ) + self.protection.set_IC_inspection = ( + 2 if st["cell_info"]["temperature_mos"] > 80 else 0 + ) self.protection.temp_high_charge = 2 if st["warnings"]["charge_overtemp"] else 0 self.protection.temp_low_charge = 2 if st["warnings"]["charge_undertemp"] else 0 - self.protection.temp_high_discharge = 2 if st["warnings"]["discharge_overtemp"] else 0 + self.protection.temp_high_discharge = ( + 2 if st["warnings"]["discharge_overtemp"] else 0 + ) return True From 973718bfadfd41caee3d13a08ddcbd012334fc98 Mon Sep 17 00:00:00 2001 From: Eike Baran Date: Sat, 7 Jan 2023 16:54:48 +0000 Subject: [PATCH 11/22] lint #3 --- etc/dbus-serialbattery/jkbms_ble.py | 27 ++++++++++++++------------- etc/dbus-serialbattery/jkbms_brn.py | 25 +++++++++++++------------ 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/etc/dbus-serialbattery/jkbms_ble.py b/etc/dbus-serialbattery/jkbms_ble.py index a5743e59..bf8318fb 100644 --- a/etc/dbus-serialbattery/jkbms_ble.py +++ b/etc/dbus-serialbattery/jkbms_ble.py @@ -10,6 +10,7 @@ class Jkbms_Ble(Battery): BATTERYTYPE = "Jkbms BLE" + def __init__(self, port, baud, address): super(Jkbms_Ble, self).__init__("zero", baud) self.type = self.BATTERYTYPE @@ -41,20 +42,20 @@ def test_connection(self): return False # device was found, presumeably a jkbms so start scraping - self.jk.start_scraping() + self.jk.start_scraping() tries = 1 - + while self.jk.get_status() == None and tries < 20: time.sleep(0.5) tries += 1 - # load initial data, from here on get_status has valid values to be served to the dbus + # load initial data, from here on get_status has valid values to be served to the dbus status = self.jk.get_status() if status == None: return False if not status["device_info"]["vendor_id"].startswith("JK-"): - return False + return False logger.info("JK BMS found!") return True @@ -87,7 +88,7 @@ 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_soc_data() # TODO: check for errors st = self.jk.get_status() @@ -96,7 +97,7 @@ def refresh_data(self): if time.time() - st["last_update"] > 30: # if data not updated for more than 30s, sth is wrong, then fail return False - + for c in range(self.cell_count): self.cells[c].voltage = st["cell_info"]["voltages"][c] @@ -109,19 +110,19 @@ def refresh_data(self): self.cycles = st["cell_info"]["cycle_count"] self.capacity = st["cell_info"]["capacity_nominal"] - #protection bits - #self.protection.soc_low = 2 if status["cell_info"]["battery_soc"] < 10.0 else 0 - #self.protection.cell_imbalance = 1 if status["warnings"]["cell_imbalance"] else 0 + # protection bits + # self.protection.soc_low = 2 if status["cell_info"]["battery_soc"] < 10.0 else 0 + # self.protection.cell_imbalance = 1 if status["warnings"]["cell_imbalance"] else 0 self.protection.voltage_high = 2 if st["warnings"]["cell_overvoltage"] else 0 self.protection.voltage_low = 2 if st["warnings"]["cell_undervoltage"] else 0 - self.protection.current_over = ( - 2 + self.protection.current_over = ( + 2 if ( - st["warnings"]["charge_overcurrent"] + st["warnings"]["charge_overcurrent"] or st["warnings"]["discharge_overcurrent"] - ) + ) else 0 ) self.protection.set_IC_inspection = ( diff --git a/etc/dbus-serialbattery/jkbms_brn.py b/etc/dbus-serialbattery/jkbms_brn.py index 24aa6b97..bca4d529 100644 --- a/etc/dbus-serialbattery/jkbms_brn.py +++ b/etc/dbus-serialbattery/jkbms_brn.py @@ -11,7 +11,7 @@ # zero means parse all incoming data (every second) CELL_INFO_REFRESH_S = 0 -DEVICE_INFO_REFRESH_S = 60*60*5 # every 5 Hours +DEVICE_INFO_REFRESH_S = 60 * 60 * 5 # every 5 Hours CHAR_HANDLE = "0000ffe1-0000-1000-8000-00805f9b34fb" MODEL_NBR_UUID = "00002a24-0000-1000-8000-00805f9b34fb" @@ -104,8 +104,8 @@ def translate(self, fb, translation, o, i=0): if i == len(translation[0]) - 1: # keep things universal by using an n=1 list kees = ( - range(0, translation[0][i]) - if isinstance(translation[0][i], int) + range(0, translation[0][i]) + if isinstance(translation[0][i], int) else [translation[0][i]] ) i = 0 @@ -114,7 +114,7 @@ def translate(self, fb, translation, o, i=0): # handle raw bytes without unpack_from; # 3. param gives no format but number of bytes val = bytearray( - fb[translation[1] + i:translation[1] + i + translation[2]] + fb[translation[1] + i : translation[1] + i + translation[2]] ) i += translation[2] else: @@ -130,8 +130,8 @@ def translate(self, fb, translation, o, i=0): o[j] = val else: if translation[0][i] not in o: - if len(translation[0]) == i+2 and isinstance( - translation[0][i+1], int + if len(translation[0]) == i + 2 and isinstance( + translation[0][i + 1], int ): o[translation[0][i]] = [None] * translation[0][i + 1] else: @@ -188,8 +188,8 @@ def decode(self): elif info_type == 0x02: if ( - CELL_INFO_REFRESH_S == 0 or - time.time() - self.last_cell_info > CELL_INFO_REFRESH_S + CELL_INFO_REFRESH_S == 0 + or time.time() - self.last_cell_info > CELL_INFO_REFRESH_S ): self.last_cell_info = time.time() info("processing frame with battery cell info") @@ -199,8 +199,8 @@ def decode(self): # power is calculated from voltage x current as # register 122 contains unsigned power-value self.bms_status["cell_info"]["power"] = ( - self.bms_status["cell_info"]["current"] - * self.bms_status["cell_info"]["total_voltage"] + self.bms_status["cell_info"]["current"] + * self.bms_status["cell_info"]["total_voltage"] ) if self.waiting_for_response == "cell_info": self.waiting_for_response = "" @@ -277,7 +277,7 @@ async def write_register( async def request_bt(self, rtype: str, client): timeout = time.time() - while self.waiting_for_response != "" and time.time()-timeout < 10: + while self.waiting_for_response != "" and time.time() - timeout < 10: await asyncio.sleep(1) print(self.waiting_for_response) @@ -337,7 +337,8 @@ def start_scraping(self): if self.is_running(): return self.bt_thread.start() - info("scraping thread started -> main thread id: " + info( + "scraping thread started -> main thread id: " + str(self.main_thread.ident) + " scraping thread: " + str(self.bt_thread.ident) From c0ea4a069aaf68821a23f5331f5c13c355c9c06d Mon Sep 17 00:00:00 2001 From: Eike Baran Date: Sat, 7 Jan 2023 17:01:32 +0000 Subject: [PATCH 12/22] lint again --- etc/dbus-serialbattery/jkbms_ble.py | 4 ++-- etc/dbus-serialbattery/jkbms_brn.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/etc/dbus-serialbattery/jkbms_ble.py b/etc/dbus-serialbattery/jkbms_ble.py index bf8318fb..995efb25 100644 --- a/etc/dbus-serialbattery/jkbms_ble.py +++ b/etc/dbus-serialbattery/jkbms_ble.py @@ -113,10 +113,10 @@ def refresh_data(self): # protection bits # self.protection.soc_low = 2 if status["cell_info"]["battery_soc"] < 10.0 else 0 # self.protection.cell_imbalance = 1 if status["warnings"]["cell_imbalance"] else 0 - + self.protection.voltage_high = 2 if st["warnings"]["cell_overvoltage"] else 0 self.protection.voltage_low = 2 if st["warnings"]["cell_undervoltage"] else 0 - + self.protection.current_over = ( 2 if ( diff --git a/etc/dbus-serialbattery/jkbms_brn.py b/etc/dbus-serialbattery/jkbms_brn.py index bca4d529..22db5f10 100644 --- a/etc/dbus-serialbattery/jkbms_brn.py +++ b/etc/dbus-serialbattery/jkbms_brn.py @@ -339,9 +339,9 @@ def start_scraping(self): self.bt_thread.start() info( "scraping thread started -> main thread id: " - + str(self.main_thread.ident) - + " scraping thread: " - + str(self.bt_thread.ident) + + str(self.main_thread.ident) + + " scraping thread: " + + str(self.bt_thread.ident) ) def stop_scraping(self): From 39345dad4e503cc0006211bdb741bb54fc70e705 Mon Sep 17 00:00:00 2001 From: Eike Baran Date: Sat, 7 Jan 2023 17:12:44 +0000 Subject: [PATCH 13/22] cleanup imports & None-check --- etc/dbus-serialbattery/jkbms_ble.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/etc/dbus-serialbattery/jkbms_ble.py b/etc/dbus-serialbattery/jkbms_ble.py index 995efb25..7a55af9c 100644 --- a/etc/dbus-serialbattery/jkbms_ble.py +++ b/etc/dbus-serialbattery/jkbms_ble.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -from battery import Protection, Battery, Cell -from utils import * -from struct import * +from battery import Battery, Cell +from utils import logger +# from struct import * from jkbms_brn import JkBmsBle from bleak import BleakScanner import asyncio @@ -30,7 +30,7 @@ def test_connection(self): loop = asyncio.get_event_loop() t = loop.create_task(BleakScanner.discover()) devices = loop.run_until_complete(t) - except BleakErrori as e: + except BleakError as e: logger.error(str(e)) return False @@ -45,13 +45,13 @@ def test_connection(self): self.jk.start_scraping() tries = 1 - while self.jk.get_status() == None and tries < 20: + while self.jk.get_status() is None and tries < 20: time.sleep(0.5) tries += 1 # load initial data, from here on get_status has valid values to be served to the dbus status = self.jk.get_status() - if status == None: + if status is None: return False if not status["device_info"]["vendor_id"].startswith("JK-"): @@ -92,7 +92,7 @@ def refresh_data(self): # result = self.read_soc_data() # TODO: check for errors st = self.jk.get_status() - if st == None: + if st is None: return False if time.time() - st["last_update"] > 30: # if data not updated for more than 30s, sth is wrong, then fail From 672192f9ba94c7a24c62d61fdb47f399ceb51d89 Mon Sep 17 00:00:00 2001 From: Eike Baran Date: Sat, 7 Jan 2023 17:17:48 +0000 Subject: [PATCH 14/22] lint --- etc/dbus-serialbattery/jkbms_ble.py | 1 - 1 file changed, 1 deletion(-) diff --git a/etc/dbus-serialbattery/jkbms_ble.py b/etc/dbus-serialbattery/jkbms_ble.py index 7a55af9c..df596d25 100644 --- a/etc/dbus-serialbattery/jkbms_ble.py +++ b/etc/dbus-serialbattery/jkbms_ble.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from battery import Battery, Cell from utils import logger -# from struct import * from jkbms_brn import JkBmsBle from bleak import BleakScanner import asyncio From 6732d417b0e409cc81defc4e1c7dda950dcd5669 Mon Sep 17 00:00:00 2001 From: Eike Baran Date: Sat, 7 Jan 2023 17:22:27 +0000 Subject: [PATCH 15/22] missing import --- etc/dbus-serialbattery/jkbms_ble.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/dbus-serialbattery/jkbms_ble.py b/etc/dbus-serialbattery/jkbms_ble.py index df596d25..d6fb4fa3 100644 --- a/etc/dbus-serialbattery/jkbms_ble.py +++ b/etc/dbus-serialbattery/jkbms_ble.py @@ -2,7 +2,7 @@ from battery import Battery, Cell from utils import logger from jkbms_brn import JkBmsBle -from bleak import BleakScanner +from bleak import BleakScanner, BleakError import asyncio import time From 60a4cf4cbc9f03d887e3f5bb8daf157a73a9b843 Mon Sep 17 00:00:00 2001 From: Eike Baran Date: Fri, 20 Jan 2023 23:43:27 +0100 Subject: [PATCH 16/22] workaround for crash of bluetooth-stack --- etc/dbus-serialbattery/jkbms_ble.py | 51 ++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/etc/dbus-serialbattery/jkbms_ble.py b/etc/dbus-serialbattery/jkbms_ble.py index d6fb4fa3..67636bea 100644 --- a/etc/dbus-serialbattery/jkbms_ble.py +++ b/etc/dbus-serialbattery/jkbms_ble.py @@ -5,7 +5,7 @@ from bleak import BleakScanner, BleakError import asyncio import time - +import os class Jkbms_Ble(Battery): BATTERYTYPE = "Jkbms BLE" @@ -25,20 +25,26 @@ def test_connection(self): # check if device with given mac is found, otherwise abort logger.info("test of jkbmsble") - try: - loop = asyncio.get_event_loop() - t = loop.create_task(BleakScanner.discover()) - devices = loop.run_until_complete(t) - except BleakError as e: - logger.error(str(e)) - return False - - found = False - for d in devices: - if d.address == self.jk.address: - found = True - if not found: - return False + tries = 0 + while True: + try: + loop = asyncio.get_event_loop() + t = loop.create_task(BleakScanner.find_device_by_address(self.jk.address)) + device = loop.run_until_complete(t) + + if device is None: + if tries > 2: + return False + else: + # device found, exit loop and continue test + break + except BleakError as e: + if tries > 2: + return False + # recover from error if tries left + logger.error(str(e)) + self.reset_bluetooth() + tries += 1 # device was found, presumeably a jkbms so start scraping self.jk.start_scraping() @@ -95,6 +101,12 @@ def refresh_data(self): return False if time.time() - st["last_update"] > 30: # if data not updated for more than 30s, sth is wrong, then fail + if self.jk.is_running(): + # if the thread is still alive but data too old there is sth + # wrong with the bt-connection; restart whole stack + self.reset_connection() + self.jk.start_scraping() + time.sleep(2) return False for c in range(self.cell_count): @@ -133,3 +145,12 @@ def refresh_data(self): 2 if st["warnings"]["discharge_overtemp"] else 0 ) return True + + def reset_bluetooth(self): + if self.jk.is_running(): + self.jk.stop_scraping() + os.system("kill -9 $(pidof bluetoothd)") + time.sleep(1) + os.system("rfkill block bluetooth") + os.system("rfkill unblock bluetooth") + os.system("/etc/init.d/bluetooth start") From 4af0a5ad01761ad7af2bc00149337810de91fd51 Mon Sep 17 00:00:00 2001 From: Eike Baran Date: Fri, 20 Jan 2023 23:49:43 +0100 Subject: [PATCH 17/22] lint --- etc/dbus-serialbattery/jkbms_ble.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/etc/dbus-serialbattery/jkbms_ble.py b/etc/dbus-serialbattery/jkbms_ble.py index 67636bea..91019a3d 100644 --- a/etc/dbus-serialbattery/jkbms_ble.py +++ b/etc/dbus-serialbattery/jkbms_ble.py @@ -7,6 +7,7 @@ import time import os + class Jkbms_Ble(Battery): BATTERYTYPE = "Jkbms BLE" @@ -29,9 +30,11 @@ def test_connection(self): while True: try: loop = asyncio.get_event_loop() - t = loop.create_task(BleakScanner.find_device_by_address(self.jk.address)) + t = loop.create_task( + BleakScanner.find_device_by_address(self.jk.address) + ) device = loop.run_until_complete(t) - + if device is None: if tries > 2: return False From 7d0e167b98be2f66dd1e49d5a87d209283fdfd8b Mon Sep 17 00:00:00 2001 From: Eike Baran Date: Sat, 21 Jan 2023 13:12:17 +0100 Subject: [PATCH 18/22] added info-message --- etc/dbus-serialbattery/jkbms_ble.py | 1 + 1 file changed, 1 insertion(+) diff --git a/etc/dbus-serialbattery/jkbms_ble.py b/etc/dbus-serialbattery/jkbms_ble.py index 91019a3d..f201a726 100644 --- a/etc/dbus-serialbattery/jkbms_ble.py +++ b/etc/dbus-serialbattery/jkbms_ble.py @@ -150,6 +150,7 @@ def refresh_data(self): return True def reset_bluetooth(self): + logger.info("reset of bluetooth triggered") if self.jk.is_running(): self.jk.stop_scraping() os.system("kill -9 $(pidof bluetoothd)") From edd9da8c0f2ef0214322244378b2ef84bba54cea Mon Sep 17 00:00:00 2001 From: Eike Baran Date: Tue, 7 Feb 2023 18:36:32 +0000 Subject: [PATCH 19/22] workaround for dying bluetooth working --- etc/dbus-serialbattery/jkbms_ble.py | 25 ++++++++++++++++++------- etc/dbus-serialbattery/jkbms_brn.py | 14 +++++++++----- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/etc/dbus-serialbattery/jkbms_ble.py b/etc/dbus-serialbattery/jkbms_ble.py index f201a726..d33bd1bf 100644 --- a/etc/dbus-serialbattery/jkbms_ble.py +++ b/etc/dbus-serialbattery/jkbms_ble.py @@ -10,6 +10,7 @@ class Jkbms_Ble(Battery): BATTERYTYPE = "Jkbms BLE" + resetting = False def __init__(self, port, baud, address): super(Jkbms_Ble, self).__init__("zero", baud) @@ -36,6 +37,7 @@ def test_connection(self): device = loop.run_until_complete(t) if device is None: + logger.info("jkbmsble not found") if tries > 2: return False else: @@ -104,13 +106,18 @@ def refresh_data(self): return False if time.time() - st["last_update"] > 30: # if data not updated for more than 30s, sth is wrong, then fail - if self.jk.is_running(): - # if the thread is still alive but data too old there is sth - # wrong with the bt-connection; restart whole stack - self.reset_connection() + logger.info("jkbmsble: bluetooth died") + + # if the thread is still alive but data too old there is sth + # wrong with the bt-connection; restart whole stack + if not self.resetting: + self.reset_bluetooth() self.jk.start_scraping() time.sleep(2) + return False + else: + self.resetting = False for c in range(self.cell_count): self.cells[c].voltage = st["cell_info"]["voltages"][c] @@ -151,10 +158,14 @@ def refresh_data(self): def reset_bluetooth(self): logger.info("reset of bluetooth triggered") - if self.jk.is_running(): - self.jk.stop_scraping() + self.resetting = True + # if self.jk.is_running(): + # self.jk.stop_scraping() + logger.info("scraping ended, issuing sys-commands") os.system("kill -9 $(pidof bluetoothd)") - time.sleep(1) + # os.system("/etc/init.d/bluetooth stop") is not enugh, kill -9 via pid is needed + time.sleep(2) os.system("rfkill block bluetooth") os.system("rfkill unblock bluetooth") os.system("/etc/init.d/bluetooth start") + logger.info("bluetooth should have been restarted") diff --git a/etc/dbus-serialbattery/jkbms_brn.py b/etc/dbus-serialbattery/jkbms_brn.py index 22db5f10..c596f2cb 100644 --- a/etc/dbus-serialbattery/jkbms_brn.py +++ b/etc/dbus-serialbattery/jkbms_brn.py @@ -11,7 +11,7 @@ # zero means parse all incoming data (every second) CELL_INFO_REFRESH_S = 0 -DEVICE_INFO_REFRESH_S = 60 * 60 * 5 # every 5 Hours +DEVICE_INFO_REFRESH_S = 60 * 60 * 10 # every 10 Hours CHAR_HANDLE = "0000ffe1-0000-1000-8000-00805f9b34fb" MODEL_NBR_UUID = "00002a24-0000-1000-8000-00805f9b34fb" @@ -87,7 +87,7 @@ class JkBmsBle: frame_buffer = bytearray() bms_status = {} - waiting_for_responsei = "" + waiting_for_response = "" last_cell_info = 0 def __init__(self, addr): @@ -308,6 +308,7 @@ async def asy_connect_and_scrape(self): client = BleakClient(self.address) print("btloop") try: + print("reconnect") await client.connect() self.bms_status["model_nbr"] = ( await client.read_gatt_char(MODEL_NBR_UUID) @@ -321,14 +322,17 @@ async def asy_connect_and_scrape(self): last_dev_info = time.time() while client.is_connected and self.run and self.main_thread.is_alive(): if time.time() - last_dev_info > DEVICE_INFO_REFRESH_S: - last_dev_info = time.time() - await self.request_bt("device_info", client) + await client.disconnect() await asyncio.sleep(0.01) except Exception as e: info("error while connecting to bt: " + str(e)) self.run = False finally: - await client.disconnect() + if client.is_connected: + try: + await client.disconnect() + except Exception as e: + info("error while disconnecting") print("Exiting bt-loop") From f07eb01361ecb414f89839dc28f5378f63544946 Mon Sep 17 00:00:00 2001 From: Eike Baran Date: Wed, 15 Feb 2023 15:34:56 +0000 Subject: [PATCH 20/22] added support for the newer 32s systems, that have slightly different frame-structure --- etc/dbus-serialbattery/jkbms_brn.py | 34 ++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/etc/dbus-serialbattery/jkbms_brn.py b/etc/dbus-serialbattery/jkbms_brn.py index c596f2cb..a6128980 100644 --- a/etc/dbus-serialbattery/jkbms_brn.py +++ b/etc/dbus-serialbattery/jkbms_brn.py @@ -56,12 +56,12 @@ TRANSLATE_CELL_INFO = [ - [["cell_info", "voltages", 16], 6, "= 112: + offset = 32 + elif translation[1] >= 54: + offset = 16 + i = 0 for j in kees: if isinstance(translation[2], int): # handle raw bytes without unpack_from; # 3. param gives no format but number of bytes val = bytearray( - fb[translation[1] + i : translation[1] + i + translation[2]] + fb[translation[1] + i + offset: translation[1] + i + translation[2] + offset] ) i += translation[2] else: val = unpack_from( - translation[2], bytearray(fb), translation[1] + i + translation[2], bytearray(fb), translation[1] + i + offset )[0] # calculate stepping in case of array i = i + calcsize(translation[2]) @@ -137,7 +145,7 @@ def translate(self, fb, translation, o, i=0): else: o[translation[0][i]] = {} - self.translate(fb, translation, o[translation[0][i]], i + 1) + self.translate(fb, translation, o[translation[0][i]], f32s=f32s, i=i + 1) def decode_warnings(self, fb): val = unpack_from(" 0) for t in TRANSLATE_CELL_INFO: - self.translate(fb, t, self.bms_status) + self.translate(fb, t, self.bms_status, f32s=has32s) self.decode_warnings(fb) debug(self.bms_status) @@ -184,6 +193,11 @@ def decode(self): info("Processing frame with settings info") if protocol_version == PROTOCOL_VERSION_JK02: self.decode_settings_jk02() + # adapt translation table for cell array lengths + ccount = self.bms_status["settings"]["cell_count"] + for i, t in enumerate(TRANSLATE_CELL_INFO): + if t[0][-2] == "voltages" or t[0][-2] == "voltages": + TRANSLATE_CELL_INFO[i][0][-1] = ccount self.bms_status["last_update"] = time.time() elif info_type == 0x02: @@ -347,11 +361,15 @@ def start_scraping(self): + " scraping thread: " + str(self.bt_thread.ident) ) - + def stop_scraping(self): self.run = False + stop = time.time() while self.is_running(): time.sleep(0.1) + if time.time() - stop > 10: + return False + return True def is_running(self): return self.bt_thread.is_alive() From 4e8bbb5cb3c1b9380bae30cdc997d4fdd4865542 Mon Sep 17 00:00:00 2001 From: Eike Baran Date: Thu, 16 Feb 2023 19:32:01 +0100 Subject: [PATCH 21/22] added stop of scraping if detection of jkbms fails --- etc/dbus-serialbattery/jkbms_ble.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/etc/dbus-serialbattery/jkbms_ble.py b/etc/dbus-serialbattery/jkbms_ble.py index d9718808..e6cb2c33 100644 --- a/etc/dbus-serialbattery/jkbms_ble.py +++ b/etc/dbus-serialbattery/jkbms_ble.py @@ -62,9 +62,11 @@ def test_connection(self): # load initial data, from here on get_status has valid values to be served to the dbus status = self.jk.get_status() if status is None: + self.jk.stop_scraping() return False if not status["device_info"]["vendor_id"].startswith("JK-"): + self.jk.stop_scraping() return False logger.info("JK BMS found!") From c734938a7232e35d631eb8e6854bc5d28504d897 Mon Sep 17 00:00:00 2001 From: Eike Baran Date: Thu, 16 Feb 2023 19:43:50 +0100 Subject: [PATCH 22/22] lint --- etc/dbus-serialbattery/jkbms_ble.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/etc/dbus-serialbattery/jkbms_ble.py b/etc/dbus-serialbattery/jkbms_ble.py index e6cb2c33..cbd1a450 100644 --- a/etc/dbus-serialbattery/jkbms_ble.py +++ b/etc/dbus-serialbattery/jkbms_ble.py @@ -118,33 +118,40 @@ def refresh_data(self): self.reset_bluetooth() self.jk.start_scraping() time.sleep(2) - + return False else: - self.resetting = False + self.resetting = False for c in range(self.cell_count): self.cells[c].voltage = st["cell_info"]["voltages"][c] self.to_temp(1, st["cell_info"]["temperature_sensor_1"]) self.to_temp(2, st["cell_info"]["temperature_sensor_2"]) - self.to_temp('mos', st["cell_info"]["temperature_mos"]) + self.to_temp("mos", st["cell_info"]["temperature_mos"]) self.current = st["cell_info"]["current"] self.voltage = st["cell_info"]["total_voltage"] self.soc = st["cell_info"]["battery_soc"] self.cycles = st["cell_info"]["cycle_count"] - + self.charge_fet = st["settings"]["charging_switch"] self.discharge_fet = st["settings"]["discharging_switch"] self.balance_fet = st["settings"]["balancing_switch"] self.balancing = False if st["cell_info"]["balancing_action"] == 0.000 else True - self.balancing_current = st["cell_info"]["balancing_current"] if st["cell_info"]["balancing_current"] < 32768 else ( 65536/1000 - st["cell_info"]["balancing_current"] ) * -1 + self.balancing_current = ( + st["cell_info"]["balancing_current"] + if st["cell_info"]["balancing_current"] < 32768 + else ( 65536/1000 - st["cell_info"]["balancing_current"] ) * -1 + ) self.balancing_action = st["cell_info"]["balancing_action"] for c in range(self.cell_count): - if self.balancing and (st["cell_info"]["max_voltage_cell"] == c or st["cell_info"]["min_voltage_cell"] == c ): + if self.balancing and ( + st["cell_info"]["max_voltage_cell"] == c + or st["cell_info"]["min_voltage_cell"] == c + ): self.cells[c].balance = True else: self.cells[c].balance = False @@ -153,9 +160,13 @@ def refresh_data(self): # self.protection.soc_low = 2 if status["cell_info"]["battery_soc"] < 10.0 else 0 # trigger cell imbalance warning when delta is to great - if st["cell_info"]["delta_cell_voltage"] > min(st["settings"]["cell_ovp"] * 0.05, 0.200): + if st["cell_info"]["delta_cell_voltage"] > min( + st["settings"]["cell_ovp"] * 0.05, 0.200 + ): self.protection.cell_imbalance = 2 - elif st["cell_info"]["delta_cell_voltage"] > min(st["settings"]["cell_ovp"] * 0.03, 0.120): + elif st["cell_info"]["delta_cell_voltage"] > min( + st["settings"]["cell_ovp"] * 0.03, 0.120 + ): self.protection.cell_imbalance = 1 else: self.protection.cell_imbalance = 0 @@ -185,7 +196,7 @@ def reset_bluetooth(self): logger.info("reset of bluetooth triggered") self.resetting = True # if self.jk.is_running(): - # self.jk.stop_scraping() + # self.jk.stop_scraping() logger.info("scraping ended, issuing sys-commands") os.system("kill -9 $(pidof bluetoothd)") # os.system("/etc/init.d/bluetooth stop") is not enugh, kill -9 via pid is needed