jblance / mpp-solar

Python package to communicate to MPP Solar PIP-4048MS inverters (and similar)
MIT License
355 stars 149 forks source link

jkbms continuous polling #251

Open sigxcpu76 opened 2 years ago

sigxcpu76 commented 2 years ago

Is there any possibility to perfom continuous polling and sending to MQTT? By continuous polling I'm referring the phone app working mode. The BLE setup is extremely slow and prone to error (sometimes I get a stacktrace instead of data).

maxxximatoze commented 1 year ago

I have the same problem, getting data over ble is very slow and not very reliable (up to 50% error). Don't know if it can be faster but it would be nice...

steveh80 commented 1 year ago

same here

sigxcpu76 commented 1 year ago

I've switched to the serial interface and everything is smooth, except that some data is missing (but can be inferred).

sugar012 commented 1 year ago

How did you do it @sigxcpu76 ?

maxxximatoze commented 1 year ago

I have the same problem, getting data over ble is very slow and not very reliable (up to 50% error). Don't know if it can be faster but it would be nice...

for those interested in getting data over bluetooth every X seconds, here is a python script I have found on github, and slitglty modified to my needs. It is outputing data every 5 seconds in a file indefinitely. You need a good bluetooth connection (short distance). For me it works great, 720 samples per hour and almost no error. Jkbms.py.txt

sugar012 commented 1 year ago

Thank you, i'm trying it out since the RS485 seems to be not working at all. I've read from another post that the new 11.XW hardware now supports 32 cells so the readings are a bit off. How do you think i can integrate this modification in your script? I got this output now:

{'model_nbr': 'BK-BLE-1.0',

'device_info': {'hw_rev': '11.XW', 'sw_rev': '11.26', 'uptime': 1218900, 'vendor_id': 'JK_B2A24S15P', 'manufacturing_date': '230409'}, 'last_update': 1682243168.0821383, 'settings': {'cell_uvp': 2.8000000000000003, 'cell_uvpr': 2.85, 'cell_ovp': 3.66, 'cell_ovpr': 3.64, 'balance_trigger_voltage': 0.003, 'power_off_voltage': 2.5, 'max_charge_current': 120.0, 'max_discharge_current': 150.0, 'max_balance_current': 120.0, 'cell_count': 16, 'charging_switch': True, 'discharging_switch': True, 'balancing_switch': True}, 'cell_info':

{'voltages': [3.378, 3.362, 3.359, 3.356, 3.356, 3.361, 3.36, 3.359, 3.356, 3.355, 3.355, 3.356, 3.358, 3.356, 3.359, 3.364], 'average_cell_voltage': 0.0, 'delta_cell_voltage': 0.0, 'max_voltage_cell': 0, 'min_voltage_cell': 0, 'resistances': [0.0, 0.0, 0.0, 65.535, 0.0, 3.359, 0.023, 0.768, 0.061, 0.062, 0.06, 0.06, 0.059000000000000004, 0.059000000000000004, 0.058, 0.058], 'total_voltage': 0.0, 'current': 0.0, 'temperature_sensor_1': 0.0, 'temperature_sensor_2': 0.0, 'temperature_mos': 0.0, 'balancing_current': 0.0, 'balancing_action': 0.0, 'battery_soc': 0, 'capacity_remain': 19857.408, 'capacity_nominal': 0.0, 'cycle_count': 53745, 'cycle_capacity': 2736.213, 'charging_switch_enabled': False, 'discharging_switch_enabled': False, 'balancing_active': False, 'error_bitmask_16': '0x0', 'error_bitmask_2': '0000000000000000', 'power': 0.0},

'warnings': {'resistance_too_high': False, 'cell_count_wrong': False, 'charge_overtemp': False, 'charge_undertemp': False, 'discharge_overtemp': False, 'cell_overvoltage': False, 'cell_undervoltage': False, 'charge_overcurrent': False, 'discharge_overcurrent': False}}

maxxximatoze commented 1 year ago

Maybe the protcol has slightly changed between my v10 hardware version and your V11, I think it can be corrected because the script seems to get some good values like the cells voltage and other stuff.

I think your best bet is to modify this section to try to get the good values where they are missing:

TRANSLATE_DEVICE_INFO = [ [["device_info", "hw_rev"], 22, "8s"], [["device_info", "sw_rev"], 30, "8s"], [["device_info", "uptime"], 38, "<L"], [["device_info", "vendor_id"], 6, "16s"], [["device_info", "manufacturing_date"], 78, "8s"], ]

TRANSLATE_SETTINGS = [ [["settings", "cell_uvp"], 10, "<L", 0.001], [["settings", "cell_uvpr"], 14, "<L", 0.001], [["settings", "cell_ovp"], 18, "<L", 0.001], [["settings", "cell_ovpr"], 22, "<L", 0.001], [["settings", "balance_trigger_voltage"], 26, "<L", 0.001], [["settings", "power_off_voltage"], 46, "<L", 0.001], [["settings", "max_charge_current"], 50, "<L", 0.001], [["settings", "max_discharge_current"], 62, "<L", 0.001], [["settings", "max_balance_current"], 50, "<L", 0.001], [["settings", "cell_count"], 114, "<L"], [["settings", "charging_switch"], 118, "4?"], [["settings", "discharging_switch"], 122, "4?"], [["settings", "balancing_switch"], 126, "4?"], ]

TRANSLATE_CELL_INFO = [ [["cell_info", "voltages", 16], 6, "<H", 0.001], [["cell_info", "average_cell_voltage"], 58, "<H", 0.001], [["cell_info", "delta_cell_voltage"], 60, "<H", 0.001], [["cell_info", "max_voltage_cell"], 62, "<B"], [["cell_info", "min_voltage_cell"], 63, "<B"], [["cell_info", "resistances", 16], 64, "<H", 0.001], [["cell_info", "total_voltage"], 118, "<H", 0.001], [["cell_info", "current"], 126, "<l", 0.001], [["cell_info", "temperature_sensor_1"], 130, "<H", 0.1], [["cell_info", "temperature_sensor_2"], 132, "<H", 0.1], [["cell_info", "temperature_mos"], 134, "<H", 0.1], [["cell_info", "balancing_current"], 138, "<H", 0.001], [["cell_info", "balancing_action"], 140, "<B", 0.001], [["cell_info", "battery_soc"], 141, "B"], [["cell_info", "capacity_remain"], 142, "<L", 0.001], [["cell_info", "capacity_nominal"], 146, "<L", 0.001], [["cell_info", "cycle_count"], 150, "<L"], [["cell_info", "cycle_capacity"], 154, "<L", 0.001], [["cell_info", "charging_switch_enabled"], 166, "1?"], [["cell_info", "discharging_switch_enabled"], 167, "1?"], [["cell_info", "balancing_active"], 191, "1?"], ]

maxxximatoze commented 1 year ago

I also have a spare V11 hardware with V11.25 software, maybe i'll give it a try to see if I have the same behavior than yours.

sugar012 commented 1 year ago

Maybe the protcol has slightly changed between my v10 hardware version and your V11, I think it can be corrected because the script seems to get some good values like the cells voltage and other stuff.

I think your best bet is to modify this section to try to get the good values where they are missing:

TRANSLATE_DEVICE_INFO = [ [["device_info", "hw_rev"], 22, "8s"], [["device_info", "sw_rev"], 30, "8s"], [["device_info", "uptime"], 38, "<L"], [["device_info", "vendor_id"], 6, "16s"], [["device_info", "manufacturing_date"], 78, "8s"], ]

TRANSLATE_SETTINGS = [ [["settings", "cell_uvp"], 10, "<L", 0.001], [["settings", "cell_uvpr"], 14, "<L", 0.001], [["settings", "cell_ovp"], 18, "<L", 0.001], [["settings", "cell_ovpr"], 22, "<L", 0.001], [["settings", "balance_trigger_voltage"], 26, "<L", 0.001], [["settings", "power_off_voltage"], 46, "<L", 0.001], [["settings", "max_charge_current"], 50, "<L", 0.001], [["settings", "max_discharge_current"], 62, "<L", 0.001], [["settings", "max_balance_current"], 50, "<L", 0.001], [["settings", "cell_count"], 114, "<L"], [["settings", "charging_switch"], 118, "4?"], [["settings", "discharging_switch"], 122, "4?"], [["settings", "balancing_switch"], 126, "4?"], ]

TRANSLATE_CELL_INFO = [ [["cell_info", "voltages", 16], 6, "<H", 0.001], [["cell_info", "average_cell_voltage"], 58, "<H", 0.001], [["cell_info", "delta_cell_voltage"], 60, "<H", 0.001], [["cell_info", "max_voltage_cell"], 62, "<B"], [["cell_info", "min_voltage_cell"], 63, "<B"], [["cell_info", "resistances", 16], 64, "<H", 0.001], [["cell_info", "total_voltage"], 118, "<H", 0.001], [["cell_info", "current"], 126, "<l", 0.001], [["cell_info", "temperature_sensor_1"], 130, "<H", 0.1], [["cell_info", "temperature_sensor_2"], 132, "<H", 0.1], [["cell_info", "temperature_mos"], 134, "<H", 0.1], [["cell_info", "balancing_current"], 138, "<H", 0.001], [["cell_info", "balancing_action"], 140, "<B", 0.001], [["cell_info", "battery_soc"], 141, "B"], [["cell_info", "capacity_remain"], 142, "<L", 0.001], [["cell_info", "capacity_nominal"], 146, "<L", 0.001], [["cell_info", "cycle_count"], 150, "<L"], [["cell_info", "cycle_capacity"], 154, "<L", 0.001], [["cell_info", "charging_switch_enabled"], 166, "1?"], [["cell_info", "discharging_switch_enabled"], 167, "1?"], [["cell_info", "balancing_active"], 191, "1?"], ]

Yep, i tried already simply adding "8" starting from "average_cell_voltage" ) but with no luck

sugar012 commented 1 year ago

I also have a spare V11 hardware with V11.25 software, maybe i'll give it a try to see if I have the same behavior than yours.

from row 21 there's this, it seems to be implemented already?:

FRAME_VERSION_JK04 = 0x01 FRAME_VERSION_JK02 = 0x02 FRAME_VERSION_JK02_32S = 0x03 PROTOCOL_VERSION_JK02 = 0x02

protocol_version = PROTOCOL_VERSION_JK02

maxxximatoze commented 1 year ago

I also have a spare V11 hardware with V11.25 software, maybe i'll give it a try to see if I have the same behavior than yours.

from row 21 there's this, it seems to be implemented already?:

FRAME_VERSION_JK04 = 0x01 FRAME_VERSION_JK02 = 0x02 FRAME_VERSION_JK02_32S = 0x03 PROTOCOL_VERSION_JK02 = 0x02

protocol_version = PROTOCOL_VERSION_JK02

Do you have tried modifying protocol_version = PROTOCOL_VERSION_JK02 to protocol_version = PROTOCOL_VERSION_JK02_32S ? Does it works ?

sugar012 commented 1 year ago

Yes i tried. I think it's the FRAME parte that Need to be modified

Il lun 24 apr 2023, 11:57 maxxximatoze @.***> ha scritto:

I also have a spare V11 hardware with V11.25 software, maybe i'll give it a try to see if I have the same behavior than yours.

from row 21 there's this, it seems to be implemented already?:

FRAME_VERSION_JK04 = 0x01 FRAME_VERSION_JK02 = 0x02 FRAME_VERSION_JK02_32S = 0x03 PROTOCOL_VERSION_JK02 = 0x02

protocol_version = PROTOCOL_VERSION_JK02

Do you have tried modifying protocol_version = PROTOCOL_VERSION_JK02 to protocol_version = PROTOCOL_VERSION_JK02_32S ? Does it works ?

— Reply to this email directly, view it on GitHub https://github.com/jblance/mpp-solar/issues/251#issuecomment-1519800061, or unsubscribe https://github.com/notifications/unsubscribe-auth/AMYZNQJ67FNLWCPN2CLCSUDXCZFAHANCNFSM57ICCTIQ . You are receiving this because you commented.Message ID: @.***>

sugar012 commented 1 year ago

I also have a spare V11 hardware with V11.25 software, maybe i'll give it a try to see if I have the same behavior than yours.

from row 21 there's this, it seems to be implemented already?: FRAME_VERSION_JK04 = 0x01 FRAME_VERSION_JK02 = 0x02 FRAME_VERSION_JK02_32S = 0x03 PROTOCOL_VERSION_JK02 = 0x02 protocol_version = PROTOCOL_VERSION_JK02

Do you have tried modifying protocol_version = PROTOCOL_VERSION_JK02 to protocol_version = PROTOCOL_VERSION_JK02_32S ? Does it works ?

any luck with this ?

alexanderfitu commented 1 year ago

Is there any further development on this? I have been unsseucfeul in getting the above script to work, as my device uses JK04 protocotol version and I cannot figure out how to define a new protocol in the linked script. If anyone has a source for that script, I may be able to fix it with the right documentation.

I am looping the script round and round to control charging and low cell volts on an EV motorbike, but the delay causes issues at low battery voltage when sag is not detected. A live stream like the JKBMS app would alleviate this.

maxxximatoze commented 1 year ago

Is there any further development on this? I have been unsseucfeul in getting the above script to work, as my device uses JK04 protocotol version and I cannot figure out how to define a new protocol in the linked script. If anyone has a source for that script, I may be able to fix it with the right documentation.

I am looping the script round and round to control charging and low cell volts on an EV motorbike, but the delay causes issues at low battery voltage when sag is not detected. A live stream like the JKBMS app would alleviate this.

Here is the version I am using right now for for some time and it works really well. It is configured to stay connected forever and to write data to a file every 5s, but you can modify it to your needs. Don't know if it works with JK04, not tried yet. Give some feedback here if you get some succes with it, it could help others...

import os 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)

logging.basicConfig(level=logging.DEBUG)

zero means parse all incoming data (every second)

CELL_INFO_REFRESH_S = 0 DEVICE_INFO_REFRESH_S = 43200 # every 12 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, "<L"], [["device_info", "vendor_id"], 6, "16s"], [["device_info", "manufacturing_date"], 78, "8s"], ]

TRANSLATE_SETTINGS = [ [["settings", "cell_uvp"], 10, "<L", 0.001], [["settings", "cell_uvpr"], 14, "<L", 0.001], [["settings", "cell_ovp"], 18, "<L", 0.001], [["settings", "cell_ovpr"], 22, "<L", 0.001], [["settings", "balance_trigger_voltage"], 26, "<L", 0.001], [["settings", "power_off_voltage"], 46, "<L", 0.001], [["settings", "max_charge_current"], 50, "<L", 0.001], [["settings", "max_discharge_current"], 62, "<L", 0.001], [["settings", "max_balance_current"], 50, "<L", 0.001], [["settings", "cell_count"], 114, "<L"], [["settings", "charging_switch"], 118, "4?"], [["settings", "discharging_switch"], 122, "4?"], [["settings", "balancing_switch"], 126, "4?"], ]

TRANSLATE_CELL_INFO = [ [["cell_info", "voltages", 16], 6, "<H", 0.001], [["cell_info", "average_cell_voltage"], 58, "<H", 0.001], [["cell_info", "delta_cell_voltage"], 60, "<H", 0.001], [["cell_info", "max_voltage_cell"], 62, "<B"], [["cell_info", "min_voltage_cell"], 63, "<B"], [["cell_info", "resistances", 16], 64, "<H", 0.001], [["cell_info", "total_voltage"], 118, "<H", 0.001], [["cell_info", "current"], 126, "<l", 0.001], [["cell_info", "temperature_sensor_1"], 130, "<H", 0.1], [["cell_info", "temperature_sensor_2"], 132, "<H", 0.1], [["cell_info", "temperature_mos"], 134, "<H", 0.1], [["cell_info", "balancing_current"], 138, "<H", 0.001], [["cell_info", "balancing_action"], 140, "<B", 0.001], [["cell_info", "battery_soc"], 141, "B"], [["cell_info", "capacity_remain"], 142, "<L", 0.001], [["cell_info", "capacity_nominal"], 146, "<L", 0.001], [["cell_info", "cycle_count"], 150, "<L"], [["cell_info", "cycle_capacity"], 154, "<L", 0.001], [["cell_info", "charging_switch_enabled"], 166, "1?"], [["cell_info", "discharging_switch_enabled"], 167, "1?"], [["cell_info", "balancing_active"], 191, "1?"], ]

class JkBmsBle:

entries for translating the bytearray to py-object via unpack

#[[py dict entry as list, each entry ] ]
frame_buffer = bytearray()
bms_status = {}

waiting_for_response = ""
last_cell_info = 0

def __init__(self, addr):
    self.address = addr
    self.bt_thread = threading.Thread(target=self.connect_and_scrape)

async def scanForDevices(self):
    devices = await BleakScanner.discover()
    for d in devices:
        print(d)

# iterative implementation maybe later due to referencing
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)
            else [translation[0][i]]
        )
        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]]
                )
                i += translation[2]
            else:
                val = unpack_from(
                    translation[2], bytearray(fb), translation[1] + i
                )[0]
                # calculate stepping in case of array
                i = i + calcsize(translation[2])
            if isinstance(val, bytes):
                val = val.decode("utf-8").rstrip(" \t\n\r\0")
            elif isinstance(val, int) and len(translation) == 4:
                val = val * translation[3]
            o[j] = val
    else:
        if translation[0][i] not in o:
            if len(translation[0]) == i + 2 and isinstance(
                translation[0][i + 1], int
            ):
                o[translation[0][i]] = [None] * translation[0][i + 1]
            else:
                o[translation[0][i]] = {}

        self.translate(fb, translation, o[translation[0][i]], i + 1)

def decode_warnings(self, fb):
    val = unpack_from("<H", bytearray(fb), 136)[0]

    self.bms_status["cell_info"]["error_bitmask_16"] = hex(val)
    self.bms_status["cell_info"]["error_bitmask_2"] = format(val, "016b")

    if "warnings" not in self.bms_status:
        self.bms_status["warnings"] = {}

    self.bms_status["warnings"]["resistance_too_high"] = bool(val & (1 << 0))
    self.bms_status["warnings"]["cell_count_wrong"] = bool(val & (1 << 2))  # ?
    self.bms_status["warnings"]["charge_overtemp"] = bool(val & (1 << 8))
    self.bms_status["warnings"]["charge_undertemp"] = bool(val & (1 << 9))
    self.bms_status["warnings"]["discharge_overtemp"] = bool(val & (1 << 15))
    self.bms_status["warnings"]["cell_overvoltage"] = bool(val & (1 << 4))
    self.bms_status["warnings"]["cell_undervoltage"] = bool(val & (1 << 11))
    self.bms_status["warnings"]["charge_overcurrent"] = bool(val & (1 << 6))
    self.bms_status["warnings"]["discharge_overcurrent"] = bool(val & (1 << 13))
    # bis hierhin verifiziert, rest zu testen

def decode_device_info_jk02(self):
    fb = self.frame_buffer
    for t in TRANSLATE_DEVICE_INFO:
        self.translate(fb, t, self.bms_status)

def decode_cellinfo_jk02(self):
    fb = self.frame_buffer
    for t in TRANSLATE_CELL_INFO:
        self.translate(fb, t, self.bms_status)
    self.decode_warnings(fb)
    debug(self.bms_status)

def decode_settings_jk02(self):
    fb = self.frame_buffer
    for t in TRANSLATE_SETTINGS:
        self.translate(fb, t, self.bms_status)
    debug(self.bms_status)

def decode(self):
    # check what kind of info the frame contains
    info_type = self.frame_buffer[4]
    if info_type == 0x01:
        info("Processing frame with settings info")
        if protocol_version == PROTOCOL_VERSION_JK02:
            self.decode_settings_jk02()
            self.bms_status["last_update"] = time.time()

    elif info_type == 0x02:
        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()
            # 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):
    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 decode")
            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):
    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
    while self.run:  # autoreconnect
        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)
            ).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():
            while client.is_connected and self.run():
                await asyncio.sleep(0.01)
        except Exception as e:
            info("error while connecting to bt: " + str(e))
            #self.run = False
        #finally:
        #    if client.is_connected:
        #        try:
        #            await client.disconnect()
        #        except Exception as e:
        #            info("error while disconnecting")

    #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
    stop = time.time()
    while self.is_running():
        time.sleep(0.5)
        if time.time() - stop > 10:
            return False
    return True

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")
#jk = JkBmsBle("C8:47:8C:E1:FA:99")
#info("sss")
#jk.start_scraping()
#while True:
    #print("asdf")
    #bms_status = {}
    #print(jk.get_status())
    #out = jk.get_status()
    #file = open('jkbmsout.txt', 'wb')
    #pickle.dump(out, file)
    #file.close()
    #with open('jkbmsout.txt', 'w', encoding='utf-8' ) as my_file:
    # my_file.write(text.decode('utf-8'))
    # my_file.close()
    #clear = lambda: os.system('clear')
    #clear()
    #time.sleep(1)

if name == "main": jk = JkBmsBle("C8:47:XX:XX:XX:XX") jk.start_scraping() while True: out = jk.get_status() print(out) with open('/root/jkbms/tmpramfs/jkbmsout.txt', 'w') as file: file.write(str(out))

time.sleep(0.5)

    time.sleep(5)
alexanderfitu commented 1 year ago

Thanks!

This version, and the one linked, generate the following error, I have updated my MAC ID to my BMS.

error while connecting to bt: Characteristic 00002a24-0000-1000-8000-00805f9b34fb was not found!

I queired the details with a BLE Scanner, and updated them:

image

However, it generates the same error below

Could not read characteristic handle 9: Protocol Error 0x02: Read Not Permitted

I have dug through the JKBMS and MPP_Solar codebases to see how they are identifying them, but as far as I am aware, they are defining each protocol manually, wheras I think this script is trying to decipher the protocol based on the answer.

I suspect the issue is that the "JK04" Protcol is not implemented in this script

alexanderfitu commented 1 year ago

I now understand that MODEL_NBR_UUID is the UUID address of the devices model name. and that CHAR_HANDLE is the UUID of the charachter mapping?

in any case, all variations did not produce a ususable result.

Zuikkis commented 1 year ago

for those interested in getting data over bluetooth every X seconds, here is a python script I have found on github, and slitglty modified to my needs. It is outputing data every 5 seconds in a file indefinitely. You need a good bluetooth connection (short distance). For me it works great, 720 samples per hour and almost no error. Jkbms.py.txt

Hey thanks for this, it works very well with my JK-B1A24S15P. :)

Zuikkis commented 1 year ago

Small improvement to the above jkbms.py. I was wondering why it keeps reconnecting, you could hear constant "beep, beep" every few seconds from the BMS. And when looking at the console I saw this line when it reconnected:

INFO:root:error while connecting to bt: 'bool' object is not callable The error is near line 330:

while client.is_connected and self.run():

self.run is a bool, not a function! So you after removing the parenthesis, error no longer appears and the script stays connected to the BMS forever. Works very well now, and no longer beeps annoyingly. :)

Mr-EJ commented 11 months ago

What if I have multiple BMSes

Do I put each bms MAC on a 'jk = JkBmsBle("XX:XX:XX:XX:XX:XX")' line?

I'm trying to get a reliable monitor for my 5 and increasing jkbmses. Tried batmon in ha but it doesn't keep a stable connection. Also tried installing mpp-solar[ble] but compilation keeps failing for bluepy.py I think.

maxxximatoze commented 2 weeks ago

For those who want to use this simple python script to get data from their jkbms V11.XX hardware, here is the corrected version that I'm currently using flawlessly to collect my data every 5 seconds. The script for V10.XX hardware version is also included if needed. JkbmsV10XXandV11XX.zip