Files
BluettiBLE/README.md
T
2026-06-04 22:53:45 +10:00

11 KiB

BluettiBLE

ESP32 library for reading and controlling Bluetti power stations over Bluetooth Low Energy (BLE).

It is a companion to VictronBLE and shares its interface philosophy — a single global object, device registration, one callback, a non-blocking loop(), and a flat parsed data struct. The key difference is the transport: Victron broadcasts data in BLE advertisements (passive, connectionless), whereas Bluetti requires an active GATT connection and a polled, Modbus-style request/response protocol. BluettiBLE therefore runs a scan → connect → poll → parse state machine under the hood.

The protocol is ported from the excellent Bluetti_ESP32_Bridge project.

Features

  • Connects to a Bluetti power station by its BLE advertised name — no pairing or key required.
  • Polls the device on a configurable interval and delivers a parsed snapshot through a single callback.
  • Reads state of charge, AC/DC input & output power, voltages, frequencies, battery pack and per-cell voltages, model and serial number.
  • Optional control: toggle AC and DC output.
  • Supports up to BLUETTI_MAX_DEVICES registered devices, connecting to one at a time in round-robin.
  • Zero external dependencies — uses the stock ESP32 Arduino BLE stack.

Three ways to read a Bluetti — and their status

Bluetti data is reachable over three different channels. This project covers all three; which one you use depends on your model's generation.

# Channel Where Status Use for
1 BluettiBLE — BLE GATT connection src/BluettiBLE.* Working (builds, on-hardware-pending per model) Older/plaintext models — full read + control
2 BluettiADV — BLE advertisement broadcast src/BluettiADV.* Ready, blocked on key Newer encrypted models — read-only, local
3 Cloud Open API — HTTPS cloud/bluetti.mjs Ready, pending account approval Any model incl. EL300 — read + control, via cloud

1. BluettiBLE — local GATT connection Working

Connects over BLE GATT and polls the plaintext Modbus-style protocol. No pairing or key. Full register read plus AC/DC output control. Works on the older / plaintext generation (AC300, AC200M, EB3A, EP500P, …). The library compiles and links; per-model field maps are ported from the reference project and want a final on-hardware sanity check. See Quick start.

2. BluettiADV — local advertisement broadcast Ready, blocked on key

Passively decodes the encrypted monitoring broadcast that newer units emit (Elite / V2 / EP600 / AC180 / AC200L …). Read-only, no connection. The decoder is implemented (AES-128-CTR + the official BLE-ADV field layout) and builds, but it needs the device's 16-byte AES key, which Bluetti has not yet exposed in the app for these models. Ready to run the moment a key is available. See Advertisement mode.

3. Cloud Open API — HTTPS Ready, pending account approval

For encrypted models (incl. the EL300) whose local channels are locked, the official cloud Open Platform works today — full read and control, no AES key. The signed client is implemented in cloud/bluetti.mjs. It needs a registered developer app + IoT API permission + the device SN authorized to your account; app approval is currently pending (Bluetti review, ~1 day). Not ESP32-native (HTTPS/cloud).

Quick decision: older Bluetti → use #1 locally. Newer/Elite (EL300) → use #3 (cloud) now, switch to #2 (local) once the broadcast key ships. If BluettiBLE connects then disconnects within seconds with no data, you have a newer encrypted unit — that's the #2/#3 path.

Supported devices

Model Enum Status (from reference project)
AC300 BLUETTI_AC300 Tested
AC200M BLUETTI_AC200M Tested (per-cell voltages)
EB3A BLUETTI_EB3A Tested
EP500P BLUETTI_EP500P Tested (per-cell voltages)
AC500 BLUETTI_AC500 Minimal / untested
EP500 BLUETTI_EP500 Untested
EP600 BLUETTI_EP600 Partial (different register map)

Hardware requirements

  • Any ESP32 (ESP32, ESP32-S3, ESP32-C3 all work) with Bluetooth.

Installation

PlatformIO

lib_deps =
    https://gitea.sh3d.com.au/Sh3d/BluettiBLE.git

Or drop the folder into your project's lib/ directory.

Arduino IDE

Copy this folder into your Arduino libraries/ directory and restart the IDE.

Quick start

#include <Arduino.h>
#include "BluettiBLE.h"

BluettiBLE bluetti;

void onBluettiData(const BluettiDevice* dev) {
    const BluettiData& d = dev->data;
    Serial.printf("\n=== %s (%s) ===\n", dev->name, d.model);
    Serial.printf("SoC: %u%%\n", d.totalBatteryPercent);
    Serial.printf("AC out: %u W   DC out: %u W\n", d.acOutputPower, d.dcOutputPower);
    Serial.printf("AC in:  %u W   DC in:  %u W\n", d.acInputPower, d.dcInputPower);
    Serial.printf("AC output: %s   DC output: %s\n",
                  d.acOutputOn ? "ON" : "off", d.dcOutputOn ? "ON" : "off");
    Serial.printf("RSSI: %d dBm\n", dev->rssi);
}

void setup() {
    Serial.begin(115200);
    delay(1000);

    bluetti.begin();
    bluetti.setDebug(false);
    bluetti.setCallback(onBluettiData);

    // The BLE name is what the unit advertises, e.g. "AC3001234567890".
    bluetti.addDevice("Shed AC300", "AC3001234567890", BLUETTI_AC300);
}

void loop() {
    bluetti.loop();   // non-blocking state machine
}

Finding your device's BLE name

Use any BLE scanner app (nRF Connect, LightBlue) and look for a device whose name begins with your model (e.g. AC300…, AC200M…, EB3A…). That full advertised name is the second argument to addDevice().

API reference

BluettiBLE();

bool   begin();
bool   addDevice(const char* name, const char* bleName, BluettiModel model);
void   setCallback(BluettiCallback cb);
void   setDebug(bool enable);
void   setPollInterval(uint32_t ms);   // default 3000
bool   isConnected() const;
size_t getDeviceCount() const;
void   loop();                         // call every loop iteration

// Control (acts on the currently connected device)
bool   setACOutput(bool on);
bool   setDCOutput(bool on);

The callback is a plain function pointer:

typedef void (*BluettiCallback)(const BluettiDevice* device);

It fires once per completed poll cycle for the connected device.

Data structures

BluettiDevice describes the device and carries the latest snapshot:

struct BluettiDevice {
    char         name[32];      // your label
    char         bleName[32];   // advertised name matched on
    BluettiModel model;
    int8_t       rssi;
    uint32_t     lastUpdate;    // millis() of last update
    bool         connected;
    bool         dataValid;
    BluettiData  data;
};

BluettiData is a flat superset of values across models. Fields a given model does not report stay at 0:

struct BluettiData {
    char     model[16];
    uint64_t serialNumber;
    float    armVersion, dspVersion;
    uint8_t  totalBatteryPercent;            // %
    uint16_t dcInputPower, acInputPower;     // W
    uint16_t acOutputPower, dcOutputPower;   // W
    float    powerGeneration;                // kWh total
    bool     acOutputOn, dcOutputOn;
    float    acInputVoltage, acInputFrequency;
    float    internalAcVoltage, internalAcFrequency;
    float    internalDcInputVoltage, internalDcInputCurrent;
    float    packVoltage;
    uint8_t  packNum, packNumMax, packBatteryPercent;
    float    cellVoltage[16];                // V, 0 if unsupported
};

How it works

  1. Scan — active BLE scan for a device advertising the Bluetti service UUID 0000ff00-… whose advertised name matches a registered device.
  2. Connect — open a GATT client, negotiate MTU 517, resolve the write (0000ff02-…) and notify (0000ff01-…) characteristics, subscribe to notify.
  3. Poll — every pollInterval, send an 8-byte read command (prefix, cmd=0x03, page, offset, count, CRC-16/MODBUS) for the next register range in the model's poll table.
  4. Parse — notification responses are decoded against the model's register map (big-endian uint16, decimals, version, serial, strings) into a rolling snapshot; a completed cycle fires the callback.

Control writes use the same frame with cmd=0x06 and the value placed big-endian in the length field.

Advertisement mode (BluettiADV)

Newer Bluetti units (Elite, *V2, EP600, AC180, AC200L, …) refuse the plaintext connection but broadcast an encrypted monitoring snapshot in their BLE advertisements — no connection, no pairing, multiple listeners, low power. This is the same scheme Victron uses. BluettiADV reads it.

You need two things from the owner's side:

  1. The 16-byte AES key (32 hex chars) — copy it from the BLUETTI app or the device's Webserver (Bluetooth-data / developer section).
  2. The device's BLE MAC address — a scanner app shows it; confirm the unit broadcasts manufacturer data starting 06 0F (company ID 0x0F06).
#include <Arduino.h>
#include "BluettiADV.h"

BluettiADV bluetti;

void onAdv(const BluettiAdvDevice* dev) {
    if (dev->lastRecordType == BLUETTI_ADV_MONITORING) {
        const auto& m = dev->monitoring;
        Serial.printf("SoC %u%%  in %u W  out %u W\n",
                      m.soc, m.inputPower, m.outputPower);
    }
}

void setup() {
    Serial.begin(115200);
    bluetti.begin(5);
    bluetti.setDebug(true);
    bluetti.setCallback(onAdv);
    bluetti.addDevice("My Elite", "AA:BB:CC:DD:EE:FF",
                      "112233445566778899aabbccddeeff00");
}

void loop() { bluetti.loop(); }

The device cycles through several record types; each callback sets dev->lastRecordType and refreshes one of the sub-structs:

  • BLUETTI_ADV_MONITORING (0x80) — SoC, in/out power, charge state, event flags
  • BLUETTI_ADV_BATTERY (0x02) — pack voltage, current, temperature, SoC
  • BLUETTI_ADV_INVERTER (0x0B) — battery V/A, AC output, PV power, yield
  • BLUETTI_ADV_CONFIG (0x81) — modes (inverter, ECO, charging), settings

This mode is read-only — there is no advertisement-based control.

Note: the advertisement bit-layout is implemented from Bluetti's official BLE ADV spec (V1.0, 2025-07-10). The exact AES nonce/counter construction and a few field offsets are best confirmed against a real packet — run with setDebug(true) (it dumps the decrypted payload) and sanity-check a value or two. Please report corrections.

Examples

  • BasicRead — connect to one (older/plaintext) device and print all values.
  • Control — read, then toggle AC/DC output on a schedule.
  • Logger — print only when values change (snapshot/change-detection pattern).
  • AdvMonitor — read a newer (encrypted) device via BluettiADV advertisements.

Adding a new model

Copy a header in src/devices/, port the register/poll/command tables from the Bluetti_ESP32_Bridge project's Device_*.h, add a BluettiModel enum value, and register it in getModelTables() in BluettiBLE.cpp.

License

MIT — see LICENSE.