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
) 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
.
Herzliche Grüße an alle,
Erik
P.S.: Wenn jemand ein bisschen Background haben möchte, gerne…