solarflow-ble
Unofficial asynchronous Python library for communicating with Zendure SolarFlow controllers over Bluetooth Low Energy.
The protocol implementation is based on
esphome-solarflow-ble.
The library has been tested with a SolarFlow 2400AC through a Home Assistant
Connect AUX-2 acting as a Bluetooth proxy. Traffic probing and debugging use
bleak-esphome.
All protocol credits and reverse-engineering efforts belong to the esphome-solarflow-ble project. This library builds on that work in Python.
Requires Python >= 3.14.
Install
uv add solarflow-ble
Usage
SolarFlowClient uses an injected transport, so applications can choose their
own Bluetooth adapter and tests can use a fake transport without Bluetooth
hardware.
import asyncio
from bleak.backends.device import BLEDevice
from solarflow_ble import BleakTransport, SolarFlowClient
async def main() -> None:
# Replace these values with a device discovered by your BLE adapter.
device = BLEDevice("AA:BB:CC:DD:EE:FF", "SolarFlow", {})
client = SolarFlowClient(BleakTransport(device))
try:
await client.connect()
print(client.state)
print(client.status)
finally:
await client.disconnect()
asyncio.run(main())
The client connects, completes the BLESPP handshake, reads the initial
getAll state, and keeps the session updated. Always call disconnect() when
the session ends.
Control methods are disabled by default. Enable them explicitly when the application is intended to change device settings:
client = SolarFlowClient(BleakTransport(device), allow_control=True)
try:
await client.connect()
await client.set_output_limit(800)
finally:
await client.disconnect()
Probe
The developer-only probe captures SolarFlow GATT traffic through an ESPHome
Bluetooth proxy (e.g. the Home Assistant Connect AUX-2).
It does not send device-setting writes by default. It sends the BLESPP
handshake, getInfo, and getAll protocol requests.
# Discover a SolarFlow device and capture its traffic.
uv run scripts/probe_solarflow.py \
--proxy "192.168.1.157" \
--noise-psk "your-esphome-noise-psk" \
--output /tmp/solarflow.jsonl
List advertisements without connecting to a SolarFlow device:
uv run scripts/probe_solarflow.py \
--proxy "192.168.1.157" \
--noise-psk "your-esphome-noise-psk" \
--list-advertisements \
--scan-seconds 30
Advertisement addresses and parsed SolarFlow identifiers are redacted by
default. Add --show-identities when selecting values for --address or
--identifier; capture files remain redacted.
Target a specific SolarFlow device by Bluetooth address:
uv run scripts/probe_solarflow.py \
--proxy "192.168.1.157" \
--noise-psk "your-esphome-noise-psk" \
--address "AA:BB:CC:DD:EE:FF" \
--scan-seconds 60 \
--capture-seconds 30 \
--output /tmp/solarflow-target.jsonl
Alternatively, target a device by its SolarFlow manufacturer-advertisement identifier:
uv run scripts/probe_solarflow.py \
--proxy "192.168.1.157" \
--noise-psk "your-esphome-noise-psk" \
--identifier "DEVICE_IDENTIFIER" \
--scan-seconds 60 \
--capture-seconds 30 \
--output /tmp/solarflow-target.jsonl
Connect to a device and capture notifications without sending the BLESPP
handshake or the initial getInfo and getAll requests:
uv run scripts/probe_solarflow.py \
--proxy "192.168.1.157" \
--noise-psk "your-esphome-noise-psk" \
--address "AA:BB:CC:DD:EE:FF" \
--no-handshake \
--capture-seconds 30 \
--output /tmp/solarflow-passive.jsonl
Development
This project uses uv and targets Python 3.14+.
uv sync
uv run python -m scripts.check
The development gate stops at the first failure in this order: format check, Ruff lint, mypy, branch-covered tests, coverage report, then package build.
Run one test file or test:
uv run pytest tests/test_solarflow.py
uv run pytest tests/test_solarflow.py -k connect_handshake
Standalone library client test
Use the standalone diagnostic to exercise SolarFlowClient through an
ESPHome Bluetooth proxy. It discovers exactly one target by address or
SolarFlow advertisement identifier and is read-only unless controls are
explicitly confirmed.
uv run scripts/test_solarflow_client.py \
--proxy "192.168.1.157" \
--noise-psk "your-esphome-noise-psk" \
--identifier "DEVICE_IDENTIFIER" \
--duration 30 \
--output /tmp/solarflow-library-test.jsonl
The script prints device identities and decoded state to stdout for local
diagnostics. The optional JSONL file is always recursively redacted. Controls
require both --controls and --confirm-controls; they also require explicit
--min-soc and --soc values because those original wire values are not
available safely for restoration.
The connection settings can be kept in the ignored local config file
scripts/test_solarflow_client.local.json. The file may contain proxy,
noise_psk, and exactly one of address or identifier:
{
"proxy": "192.168.1.157",
"noise_psk": "your-esphome-noise-psk",
"identifier": "DEVICE_IDENTIFIER"
}
Run the diagnostic with the default local file:
uv run scripts/test_solarflow_client.py --duration 30
Use --config path/to/config.json for another local file. Command-line values
override values from the config file, so individual settings can be replaced
without editing it:
uv run scripts/test_solarflow_client.py \
--config scripts/test_solarflow_client.local.json \
--identifier "OTHER_DEVICE_IDENTIFIER"
Do not commit this file or paste a real noise_psk into documentation,
fixtures, logs, or shell history. The default local filename is ignored by
Git; use a file with equivalent local-only handling when choosing another
config path. The script never prints or writes noise_psk.
License
This project is licensed under the MIT License. See LICENSE.
Metadata
Release files for solarflow-ble 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| solarflow_ble-0.1.1.tar.gz | 9.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| solarflow_ble-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 20.9 kB
Release files / solarflow_ble-0.1.1.tar.gz
| Download URL | solarflow_ble-0.1.1.tar.gz |
|---|---|
| Size | 9.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c4ced8c0fdd455345b09ad3dc3e1e572eeef447fa2da118d6609daaf4eef95db
|
|
BLAKE2b-256 checksum How to use checksums |
86677174934c6503e53aaa24526081cb15d4654d9b6ca3a859d7ebd55005ed51
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.
Transparency logRelease files / solarflow_ble-0.1.1-py3-none-any.whl
| Download URL | solarflow_ble-0.1.1-py3-none-any.whl |
|---|---|
| Size | 11.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
729481aa2e19d3e2fb572cfbe1fb112fc095df17bd8939aa384510f98af265a9
|
|
BLAKE2b-256 checksum How to use checksums |
aa97b27de1986a03c428555a58dfb0206127e161c09a82ddb1b21e716dfee86a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.
Transparency log