Metadata-Version: 2.4
Name: solarflow-ble
Version: 0.2.3
Summary: Asynchronous Python library for Zendure SolarFlow Bluetooth Low Energy devices
Keywords: solarflow,zendure,ble,bluetooth,bleak,asyncio,home-assistant
Author: Daan Vervacke
Author-email: Daan Vervacke <daan.vervacke@proton.me>
License-Expression: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: System :: Hardware
Classifier: Typing :: Typed
Requires-Dist: bleak>=1.1.1,<4
Requires-Dist: bleak-retry-connector>=4.7.1
Requires-Python: >=3.14
Project-URL: Homepage, https://github.com/DaanVervacke/solarflow-ble
Project-URL: Repository, https://github.com/DaanVervacke/solarflow-ble
Project-URL: Issues, https://github.com/DaanVervacke/solarflow-ble/issues
Project-URL: Documentation, https://solarflow-ble.readthedocs.io/
Project-URL: Changelog, https://github.com/DaanVervacke/solarflow-ble/blob/main/CHANGELOG.md
Description-Content-Type: text/markdown

# solarflow-ble

[![Check](https://github.com/DaanVervacke/solarflow-ble/actions/workflows/check.yml/badge.svg)](https://github.com/DaanVervacke/solarflow-ble/actions/workflows/check.yml)
[![PyPI version](https://img.shields.io/pypi/v/solarflow-ble.svg)](https://pypi.org/project/solarflow-ble/)
[![Python versions](https://img.shields.io/pypi/pyversions/solarflow-ble.svg)](https://pypi.org/project/solarflow-ble/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

Async Python library for Zendure SolarFlow devices over Bluetooth Low Energy.
Requires Python >= 3.14.

> Unofficial and reverse-engineered: not endorsed by Zendure, and it may
> break without notice whenever Zendure changes their firmware.

The protocol work builds on
[esphome-solarflow-ble](https://github.com/krumpholz/esphome-solarflow-ble)
and reverse engineering of the Zendure Android app. The library has been
tested with a SolarFlow 2400AC through a Home Assistant Connect AUX-2
Bluetooth proxy.

## Install

```bash
uv add solarflow-ble
```

## Documentation

The API reference is hosted at
[solarflow-ble.readthedocs.io](https://solarflow-ble.readthedocs.io/).

## Usage

`SolarFlowClient` accepts an injected `BleTransport`. Tests can use a fake
transport without Bluetooth hardware.

```python
import asyncio

from bleak.backends.device import BLEDevice

from solarflow_ble import BleakTransport, SolarFlowClient


async def main() -> None:
    device = BLEDevice("AA:BB:CC:DD:EE:FF", "SolarFlow", {})
    async with SolarFlowClient(BleakTransport(device)) as client:
        print(client.device_id)
        print(client.state)


asyncio.run(main())
```

The `async with` block connects on entry and always disconnects on exit,
including when the body raises. For reconnect use cases, call `connect()`
and `disconnect()` yourself instead; `connect()` is reusable after
`disconnect()` on the same client instance.

The client waits for `BLESPP`, sends `BLESPP_OK`, requests `getInfo`, then
sends a `read` request for `getAll`. It keeps the session updated with report
messages.

Control methods are disabled unless `allow_control=True`. Validated ranges are:

- input and output limits: `0..2400 W`
- minimum SOC: `0..50%`
- target SOC: `70..100%`
- AC mode: `1` or `2`

### Model limits

Those ranges are the values verified for the SolarFlow 2400AC and are the
default for every model. Validation bounds are model-specific: applying
2400AC bounds to a smaller unit could forward out-of-spec values to the
hardware, while larger models would have valid values rejected. Pass
`model="solarflow-2400ac"` (matched case-insensitively against the registry
of verified entries; the `productKey` the device reports is used when
`model` is not given) or explicit `limits=SolarFlowLimits(...)` to the
constructor. At validation time the client resolves bounds in this order:
explicit `limits`, the registry entry for the model, then the 2400AC
default with a one-time warning. Only bounds verified against real
hardware are registered; unverified models always fall back to the
default with that warning.

## Connection loss

The client detects a failed session through the next failing BLE write (the
keepalive writes every 30 seconds by default, so an abrupt disconnect is
detected at most one keepalive interval later) and through client-side
protocol errors such as invalid JSON payloads or device ID mismatches. When a
session fails:

- pending calls raise `SolarFlowConnectionError` immediately instead of
  waiting for the response timeout
- `client.status` flips to `ConnectionStatus.DISCONNECTED`
- an optional `connection_lost_callback` receives the underlying exception
- `client.last_error` holds the most recent device-reported error message,
  if the device sent one (`method == "error"`)

Messages that are still being processed when the session fails keep flowing
to `update_callback`, with `update.status` reflecting `DISCONNECTED`. Call
`connect()` again to start a fresh session.

## Probe

The developer-only probe captures raw GATT traffic through an ESPHome
Bluetooth proxy. It never sends device-setting writes.

List advertisements:

```bash
uv run scripts/probe_solarflow.py \
  --proxy "the-ip-of-your-esphome-bluetooth-proxy" \
  --noise-psk "the-encryption-key-of-your-esphome-bluetooth-proxy" \
  --list-advertisements \
  --show-identities
```

Capture a target by address or SolarFlow advertisement identifier:

```bash
uv run scripts/probe_solarflow.py \
  --proxy "the-ip-of-your-esphome-bluetooth-proxy" \
  --noise-psk "the-encryption-key-of-your-esphome-bluetooth-proxy" \
  --identifier "DEVICE_IDENTIFIER" \
  --capture-seconds 30 \
  --output /tmp/solarflow.jsonl
```

Use `--no-handshake` for passive notification capture. Addresses, identifiers,
device IDs, product keys, pack serials, and credentials are redacted from
capture files. `--show-identities` affects logs only.

The probe uses the same `SolarFlowClient` protocol flow as the library in
normal mode. Passive mode connects directly to the notification characteristic
without sending protocol writes.

## Standalone library test

`scripts/test_solarflow_client.py` exercises the library directly. It does not
use the probe script. The default run is read-only and requires exactly one of
`--address` or `--identifier`.

```bash
uv run scripts/test_solarflow_client.py \
  --proxy "the-ip-of-your-esphome-bluetooth-proxy" \
  --noise-psk "the-encryption-key-of-your-esphome-bluetooth-proxy" \
  --identifier "DEVICE_IDENTIFIER" \
  --duration 30 \
  --output /tmp/solarflow-library-test.jsonl
```

You can put the connection settings in the ignored file
`scripts/test_solarflow_client.local.json`:

```json
{
  "proxy": "the-ip-of-your-esphome-bluetooth-proxy",
  "noise_psk": "the-encryption-key-of-your-esphome-bluetooth-proxy",
  "identifier": "DEVICE_IDENTIFIER"
}
```

Then run:

```bash
uv run scripts/test_solarflow_client.py --duration 30
```

The script prints decoded updates to stdout and always redacts optional JSONL
output. Controls require both `--controls` and `--confirm-controls`. Never
commit the local config or a real PSK.

## Development

See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, the gate, and the pull
request expectations.

```bash
uv sync
uv run python -m scripts.check
```

Run focused tests:

```bash
uv run pytest tests/test_solarflow.py
uv run pytest tests/test_solarflow_client_script.py
```

The full gate runs a version drift check, format, Ruff, mypy, branch-covered
tests, coverage, `uv build`, and `uv audit` in that order.

## License

MIT. See [LICENSE](LICENSE).
