Tester gesucht: Ective BMS via Bluetooth

Ausgangslage

Ich besitze eine Lithium Batterie von Ective und wollte die dringend in mein Van Pi System mit aufnehmen. Ich hab deshalb das GATT BLE Protokoll untersucht und mit Dekompilieren der Ective App ein funktionierendes Python-Skript entwickelt um sämtliche Parameter auszulesen. Code findet ihr hier.

Testen

Da die Leute hinter PeKaWay aktuell keine Batterie des Herstellers zur Verfügung haben, dachte ich wir könnten in der Community testen. Wer also so eine Batterie hat, soll das doch mal ausprobieren. Ich helfe auch gerne, wenn es Fragen gibt.

Grobe Anleitung

  1. Python File herunterladen
curl https://raw.githubusercontent.com/TKone7/ective-ble/main/ectiveBms.py -o pekaway/ble_py/ectiveBms.py
  1. In NodeRED folgende Nodes anpassen

  1. Dann gewünschte MAC Adresse über bestehendes Menü (Config > System > Bluetooth > Liontron/JBD Bat) setzen

Achtung: Damit überschreibt ihr die Funktionen für das bereits integrierte Bluetooth BMS.

Ich freue mich auf eure Resultate.

Gruss
Tobi

4 Likes

Hallo,

habe 3 Ective Akkus verbaut , könnte mich demnächst mal dran setzten.

MFG Bernd

Hey, cool! Lass mich wissen, wenn du hilfe brauchst.

Hallo,

hatte dir schon eine Mail geschrieben.
Wenn du mir bei der Software etwas hilft können wir es angehen.

MFG Bernd

Hallo TKone7,

ich hab dein Python-Script versucht zum laufen zu bringen, aber leider konnte ich keine Verbindung zum Ective BMS herstellen. Zum einen kann ich unter den ganzen MAC-Adressen das richtige Gerät nicht finden. Zum anderen kann ich zu all den MAC-Adressen keine Verbindung aufbauen. Hab es auch schon über PUTTY und gatttool probiert. Wie bist du da vorgegangen?

Viele Grüße
Dennis

Hi Dennis

Freut mich, dass du die Ective BMS Integration testen möchtest. Um das richtige Gerät zu finden, empfehle ich dir, dich mit Putty (oder anderen SSH Tool) auf das Raspi zu verbinden. Dann führst du den Scan manuell aus:

$ sudo hcitool lescan

Es werden dann vermutlich viele Geräte wiederholt angezeigt. Um den Scan zu beenden, betätige <Ctrl-C>. Du solltest unter den Resultaten ein Gerät finden, dass den selben Namen hat, wie der, der in der Ective mobile App angezeigt wird.

Wenn du glaubst die richtige MAC Adresse gefunden zu haben, kannst du mit dem gatttool einen einfachen “connect” versuchen:

$ sudo gatttool -b <deine-MAC-adresse> -I

Damit landest du in einer interaktiven Konsole mit Gatt-Kommandos. Gib dann den Verbindungs-Befehl ein:

[__:__:__:__:__:__][LE]> connect
Attempting to connect to __:__:__:__:__:__
Connection successful

Betätige <Ctrl-D> um das Gatt-Tool zu beenden.

Wenn das alles klappt, sollte das Python-Skript eigentlich laufen.

Gruss
Tobi

Hallo Tobi,

danke für die ausführliche Anleitung. Ich konnte mittlerweile die richten MAC-Adressen auswählen, nur leider kann ich über das Gatt-Tool keine Verbindung aufbauen. Anbei die Fehlermeldung:

pi@pekaway:~$ sudo gatttool -b __:__:__:__:__:__ -I
[__:__:__:__:__:__][LE]> connect
Attempting to connect to __:__:__:__:__:__
Error: connect error: Function not implemented (38)

Bisher konnte ich keine Lösung für den Fehler im Netz finden. Woran denskt du könnte es liegen?

Grüße
Dennis

Hi Dennis

In der Tat hatte ich mit ganz ähnlichen Problemen zu kämpfen. Konnte lange nichts finden, bis ich auf einige Forum-Beiträge gestossen bin, die sagten, es kann zu Störungen kommen, da das Raspi sowohl WiFi als BLE auf dem selben Chip verbaut hat. Anscheinend operiert Bluetooth auf einem sehr ähnlichen Spektrum wie WiFi 2.4 und so kann es da zu Problemen kommen speziell, wenn die Empfänger sehr nah beieinander auf der Platine verbaut sind.

Um diesen Verdacht zu bestätigen habe ich das 2.4 GHz Band des RPi deaktiviert und nur noch mit dem 5 GHz gearbeitet, und siehe da, die Probleme sind verschwunden. Um nicht von diesem “Hack” abhängig zu sein, habe ich mir am Schluss einen kleinen USB-Bluetooth-Dongle gekauft. Seither auch keine Probleme mehr.

Gruss
Tobi

Hallo zusammen,

ich wollte mal fragen ob es neue Kenntnisse zu dem Projekt gibt?

ich habe jetzt auch mal versucht das Projekt zu testen, allerdings mit wenig Erfolg. Da ich nicht wusste was man auf der Node-RED Oberfläche alles anpassen muss habe ich ectiveBms.py wie in der README beschrieben ist ($ python3 ectiveBms.py -v -d EE:EE:EE:EE:EE:EE), gestartet.
Anschließend habe ich folgende Fehlermeldung erhalten:

henni@henni-IdeaPad:~/Documents/5_semester/studienarbeit/batterie/ective-ble$ python3 ectiveBms.py -v -d 30:55:44:37:AA:51
Trying to connect to 30:55:44:37:AA:51
Subscribe for notifications
Traceback (most recent call last):
File “/home/henni/Documents/5_semester/studienarbeit/batterie/ective-ble/ectiveBms.py”, line 140, in
p.writeCharacteristic(int.from_bytes(writeHandle, ‘big’), b’\x01\x00’, True)
File “/home/henni/Documents/5_semester/studienarbeit/batterie/ective-ble/venv/lib/python3.11/site-packages/bluepy/btle.py”, line 543, in writeCharacteristic
return self._getResp(‘wr’)
^^^^^^^^^^^^^^^^^^^
File “/home/henni/Documents/5_semester/studienarbeit/batterie/ective-ble/venv/lib/python3.11/site-packages/bluepy/btle.py”, line 407, in _getResp
resp = self._waitResp(wantType + [‘ntfy’, ‘ind’], timeout)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File “/home/henni/Documents/5_semester/studienarbeit/batterie/ective-ble/venv/lib/python3.11/site-packages/bluepy/btle.py”, line 368, in _waitResp
raise BTLEGattError(“Bluetooth command failed”, resp)
bluepy.btle.BTLEGattError: Bluetooth command failed (code: 3, error: Attribute can’t be written)

Mit $ sudo gatttool -b <deine-MAC-adresse> -I konnte ich mich mit dem BMS verbinden. Die Frequenzbänder habe ich nicht geändert und zum Auslesen habe ich meinen Laptop verwendet.

Hat jemand eine Idee?

Grüße

Henrik

Hi Henrik
Tut mir leid, dass es hier nicht auf Anhieb klappte. Tatsächlich scheint es, als ob die neue Version Ective BT Batterien ein leicht anderes Protokoll implementieren.

Ich habe nämlich mein Batterie-Setup vergrössert und eine zweite baugleiche Batterie dazugekauft. Mit der einen (alten) kann ich mich Problemlos verbinden, mit der anderen jedoch nicht und erhalte den ähnlichen/selben Fehler wie du.

Ich werde mich in den nächsten Wochen nochmal dahinter setzen und versuche eine Version zu schreiben, welche für beide Modelle funktioniert.

Hey Tobi,
Wäre aufjedenfall mega wenn das funktionieren würde und man die Batterie einbinden kann. Ich will es aufjedenfall auch mal selbst ausprobieren. Hast du dazu Tipps wie du die Ecitve App Dekompiliert hast?
VG
Henrik

habe auch ein Ective Batterie läuft das schon oder kann ich Helfen?

Ja es funktioniert bei mir ziemlich zuverlässig. Der Code ist online. Ich starte das Tool im Docker Container und es veröffentlich die Daten dann via MQTT.

Hallo,
ist es auch möglich damit die ective accubox auszulesen?
Wäre Super wenn das gehen würde

Moin Moin,
Nochmals ne frage zu diesem Code
ich hab es geschafft über gatttool die große Accubox von ective zu verbinden.
Aber er zeigt mir noch nichts an habe hoffentlich die richtigen nodes angepasst.
Ich frag mich was ich falsch mache :frowning:
Für Hilfe wäre ich echt Dankbar :slight_smile:

Habe jetzt nochmal bisschen rum probiert mit gattool bekomme ich eine verbindung aber versuche ich das script auszuführen kommt nur waiting und dann bricht er ab.
Woran kann das liegen?

Keine Antworten?
Das ist ja schade

Hallo zusammen, ich habe auch eine Accubox S200 und wollte den Status für mein Cockpit Dashboard auslesen können. Habe mal mit dem nRF52840 + Wireshark probiert sie zu (also als absoluter hobbyist :wink: ) zu reverse engineeren und bin nach und nach zu diesem Skript gekommen

#!/usr/bin/env python3
"""
AccuBox BLE JSON Reader
Supports one‑time and cyclic queries, automatic mode detection,
and sign for current/power (charging positive, discharging negative).

ummeegge 27.04.2026
"""

import asyncio
import json
import argparse
import sys
from datetime import datetime
from bleak import BleakScanner, BleakClient

# ========== CONFIGURATION ==========
# Default MAC address of your AccuBox (can be overridden via --mac)
DEFAULT_MAC = "58:b6:4f:49:9c:f1"

# BLE characteristic UUID and command
CHAR_UUID = "0000ffe1-0000-1000-8000-00805f9b34fb"
CMD_REQUEST_STATUS = bytes.fromhex("1c030400000846b1")

# Scaling factors (empirically determined)
VOLT_SCALING_CHARGE = 47.3      # charging mode (raw > 200)
VOLT_SCALING_DISCHARGE = 10.0   # discharging mode (raw < 200)
CURRENT_SCALING_CHARGE = 94.0   # current from bytes 12-13 (charging)
CURRENT_SCALING_DISCHARGE = 7965.0  # current from bytes 18-19 (discharging)

# ========== DECODING LOGIC ==========
def decode_status(data: bytes) -> dict:
    """Decode the 21‑byte notification into a status dictionary."""
    if len(data) != 21:
        return {"status": "error", "message": f"Unexpected length: {len(data)}"}

    # Basic fields (little‑endian)
    capacity_ah = int.from_bytes(data[4:6], 'little')
    soc_percent = int.from_bytes(data[6:8], 'little')
    rest_hours = int.from_bytes(data[8:10], 'little')
    rest_minutes = int.from_bytes(data[10:12], 'little')
    temp_raw = int.from_bytes(data[14:16], 'little')
    voltage_raw = int.from_bytes(data[16:18], 'little')

    # Raw current values (signed)
    current_charge_raw = int.from_bytes(data[12:14], 'little', signed=True)
    current_discharge_raw = int.from_bytes(data[18:20], 'little', signed=True)

    # Mode detection using voltage raw value
    if voltage_raw > 200:          # charging mode
        mode = "charging"
        voltage_v = round(voltage_raw / VOLT_SCALING_CHARGE, 2)
        # Use absolute value of current (always positive when charging)
        current_a = round(abs(current_charge_raw) / CURRENT_SCALING_CHARGE, 2)
    else:                          # discharging mode
        mode = "discharging"
        voltage_v = round(voltage_raw / VOLT_SCALING_DISCHARGE, 2)
        # Force negative sign for discharging
        current_a = - round(abs(current_discharge_raw) / CURRENT_SCALING_DISCHARGE, 2)

    # Power (sign follows current)
    power_w = round(abs(current_a) * voltage_v, 1)
    if current_a < 0:
        power_w = -power_w

    return {
        "status": "ok",
        "timestamp": datetime.now().isoformat(),
        "capacity_ah": capacity_ah,
        "soc_percent": soc_percent,
        "rest_time": f"{rest_hours:02d}:{rest_minutes:02d}",
        "voltage_v": voltage_v,
        "current_a": current_a,
        "power_w": power_w,
        "mode": mode,
        "temp_raw": temp_raw,
        "raw_hex": data.hex()
    }

# ========== ONE‑TIME QUERY ==========
async def read_once(mac: str):
    """Connect, send command once, print one JSON status."""
    device = await BleakScanner.find_device_by_address(mac)
    if not device:
        print(json.dumps({"status": "error", "message": f"Device {mac} not found"}))
        return

    async with BleakClient(device) as client:
        # Find the characteristic by UUID
        target_char = None
        for service in client.services:
            for char in service.characteristics:
                if char.uuid == CHAR_UUID:
                    target_char = char
                    break
            if target_char:
                break

        if not target_char:
            print(json.dumps({"status": "error", "message": "Characteristic not found"}))
            return

        result = None
        def handler(sender, data):
            nonlocal result
            result = decode_status(data)

        await client.start_notify(target_char, handler)
        await asyncio.sleep(0.2)
        await client.write_gatt_char(target_char, CMD_REQUEST_STATUS, response=False)

        # Wait for notification (max 3 seconds)
        for _ in range(30):
            if result:
                break
            await asyncio.sleep(0.1)

        await client.stop_notify(target_char)

        if result:
            print(json.dumps(result))
        else:
            print(json.dumps({"status": "error", "message": "Timeout"}))

# ========== CYCLIC QUERY ==========
async def cyclic_query(mac: str, interval_sec: float):
    """Continuously query the battery every `interval_sec` seconds."""
    print(json.dumps({"status": "info", "message": f"Starting cyclic query every {interval_sec} seconds for {mac}"}))

    device = await BleakScanner.find_device_by_address(mac)
    if not device:
        print(json.dumps({"status": "error", "message": f"Device {mac} not found"}))
        return

    async with BleakClient(device) as client:
        target_char = None
        for service in client.services:
            for char in service.characteristics:
                if char.uuid == CHAR_UUID:
                    target_char = char
                    break
            if target_char:
                break

        if not target_char:
            print(json.dumps({"status": "error", "message": "Characteristic not found"}))
            return

        result = None
        def handler(sender, data):
            nonlocal result
            result = decode_status(data)

        await client.start_notify(target_char, handler)

        try:
            while True:
                result = None
                await client.write_gatt_char(target_char, CMD_REQUEST_STATUS, response=False)
                # Wait up to 2 seconds for a notification
                for _ in range(20):
                    if result:
                        break
                    await asyncio.sleep(0.1)
                if result:
                    print(json.dumps(result))
                else:
                    print(json.dumps({"status": "error", "message": "Timeout"}))
                await asyncio.sleep(interval_sec)
        except asyncio.CancelledError:
            pass
        finally:
            await client.stop_notify(target_char)

# ========== COMMAND LINE INTERFACE ==========
def main():
    parser = argparse.ArgumentParser(description="AccuBox BLE JSON Reader")
    parser.add_argument("--mode", choices=["once", "cyclic"], default="once",
                        help="Query mode: once (single read) or cyclic (continuous)")
    parser.add_argument("--interval", type=float, default=5.0,
                        help="Interval in seconds for cyclic mode (default: 5)")
    parser.add_argument("--mac", type=str, default=DEFAULT_MAC,
                        help=f"BLE MAC address of the AccuBox (default: {DEFAULT_MAC})")
    args = parser.parse_args()

    if args.mode == "once":
        asyncio.run(read_once(args.mac))
    else:
        try:
            asyncio.run(cyclic_query(args.mac, args.interval))
        except KeyboardInterrupt:
            sys.exit(0)

if __name__ == "__main__":
    main()

. Ich weiß nicht ob es von nutzen für jede Accubox ist oder ob es bei neueren (meine ist 3 Jahre alt) Änderungen im Protokol gab.

Das Skript hat ein paar Optionen

╰─➤  ./accubox-json.py --helpusage: accubox-json.py [-h] [–mode {once,cyclic}] [–interval INTERVAL] [–mac MAC]

derzeit ist noch meine MAC hardgecodet drinne aber man kann sie mittels dem --mac Flag überschreiben mit der eigenen. Es gibt zwei Modi zum Auslesen des Statuses, einmal das einfache auslesen mit dem –mode once was dann so aussieht

╰─➤  ./accubox-json.py --mode once{“status”: “ok”, “timestamp”: “2026-05-10T10:38:11.927463”, “capacity_ah”: 154, “soc_percent”: 77, “rest_time”: “130:57”, “voltage_v”: 13.3, “current_a”: -0.61, “power_w”: -8.1, “mode”: “discharging”, “temp_raw”: 118, “raw_hex”: “1c0310009a004d00820039000100760085000fed1c”}

und eine zyklische Abfrage –mode cyclic wo man den Intervall setzen kann (default 5 Sekunden) , was dann ca. so aussieht

╰─➤  ./accubox-json.py --mode cyclic
{"status": "info", "message": "Starting cyclic query every 5.0 seconds for 58:b6:4f:49:9c:f1"}
{"status": "ok", "timestamp": "2026-05-10T10:38:54.925653", "capacity_ah": 154, "soc_percent": 77, "rest_time": "111:58", "voltage_v": 13.2, "current_a": -2.7, "power_w": -35.6, "mode": "discharging", "temp_raw": 138, "raw_hex": "1c0310009a004d006f003a0001008a00840012545e"}
{"status": "ok", "timestamp": "2026-05-10T10:39:00.337796", "capacity_ah": 154, "soc_percent": 77, "rest_time": "70:14", "voltage_v": 13.3, "current_a": -3.67, "power_w": -48.8, "mode": "discharging", "temp_raw": 181, "raw_hex": "1c0310009a004d0046000e000100b5008500187201"}
{"status": "ok", "timestamp": "2026-05-10T10:39:05.796823", "capacity_ah": 154, "soc_percent": 77, "rest_time": "103:41", "voltage_v": 13.4, "current_a": -2.64, "power_w": -35.4, "mode": "discharging", "temp_raw": 149, "raw_hex": "1c0310009a004d00670029000100950086001352cc"}

. Die eigene MAC für die Accubox zusetzen wäre dann ein Befehl in dieser Art:

╰─➤  ./accubox-json.py --mode once --mac 58:b6:4f:49:9c:f1
{"status": "ok", "timestamp": "2026-05-10T10:39:37.980227", "capacity_ah": 154, "soc_percent": 77, "rest_time": "110:21", "voltage_v": 13.3, "current_a": -2.99, "power_w": -39.8, "mode": "discharging", "temp_raw": 185, "raw_hex": "1c0310009a004d006e0015000100b9008500185d50"}

. Ich hoffe dass das Skript bei dir @Coldwars funktioniert (Optimieren ist immer drinne und ich denke auch nötig) und das es vielleicht auch für den Topic ersteller @TKone7 interessant ist, übrigens Danke für deine Arbeit die doch die nötige Inspiriation bei mir gebracht hat die Accubox von Ecitve mal unter die Lupe zu nehmen :slight_smile: .

Herzliche Grüße an alle,

Erik

P.S.: Wenn jemand ein bisschen Background haben möchte, gerne…

1 Like

Hi
freut mich zu lesen das du es geschafft hast eine Akkubox von Ective auszulesen. Ich habe eine 300S welche ich auch gerne auslesen und anzeigen möchte.
Bin aber ein absolutes Greenhorn was Computer angeht.
Wie kann ich dein Script zum laufen kriegen?

Danke Klaus

Hallo Klaus, ich habe dir mal in deinem alten Topic geantwortet da er doch AccuBox spezifisch ist und wir hier nicht zuweit Off Topic kommen.

Beste Grüße,

Erik

1 Like