Files
bmspy/README.md
T
tim 0c179d29e5 Disconnect from Bluetooth devices before the server stops
The polling threads are daemon threads, so a stop ended the server in the middle of a Bluetooth read. The read got no disconnect, and the adapter kept an open connection or a scan in progress.

The server now sets the stop event at exit, closes its socket, and waits at most 20 seconds for the polling threads. A Bluetooth read that sees the stop event cancels its current step, disconnects, and returns. A read that waits for the adapter does not connect after a stop.
2026-10-10 11:47:16 +02:00

206 lines
8.0 KiB
Markdown

# bmspy
bmspy is a tool to get information from battery management systems and UPS devices. It supports:
- [Xiaoxiang-type](https://www.lithiumbatterypcb.com/product/4s-or-3s-12v-li-ion-or-lifepo4-battery-smart-bms-with-bluetooth-function-uart-and-rs485-communication-with-60a-to-120a-constant-current/?attribute_specification-selection=4S+Lifepo4+120A+with+UART+and+RS485) BMS devices via serial (USB)
- EcoFlow power stations via Bluetooth LE
It can display the information as text, in JSON, or export the data continuously to InfluxDB or a Prometheus exporter.
Multiple devices can be connected at once. Each is identified by a name. Data from all of them (or just one) can be pushed to InfluxDB or Prometheus in the same connection. Each measurement is tagged with the device name.
Install [uv](https://docs.astral.sh/uv/getting-started/installation/) and use Python 3.12 or later, below Python 4.
Then create the local environment:
```bash
git clone https://git.treehouse.org.za/tim/bmspy
cd bmspy
uv sync --locked
```
To install with EcoFlow Bluetooth LE support:
```bash
uv sync --locked --extra ecoflow-bt
```
To install with InfluxDB and Prometheus support:
```bash
uv sync --locked --extra influxdb --extra prometheus
```
These optional dependency sets are called extras. Select the required extras
with each `uv run` command, or use `--all-extras` to enable all of them.
`uv sync` also installs development dependencies by default. Use `--no-dev`
for a deployment that does not need the test tools.
## Running the server
The server daemon reads from one or more devices and makes the data available over a Unix socket.
Single JBD BMS device (defaults to `/dev/ttyUSB0`):
uv run bmspy-server
Multiple JBD BMS devices, with optional names (default name is derived from the device path, for example `ttyUSB0`):
uv run bmspy-server -d network:/dev/ttyUSB0 -d nas:/dev/ttyUSB1
### EcoFlow Bluetooth LE devices
Install the `ecoflow-bt` extra as shown above. Close the EcoFlow app and stop other
Bluetooth collectors before connecting to a device.
#### Setup
Get your EcoFlow account user ID:
```bash
uv run bmspy-ecoflow-login you@example.com
```
The command prompts for your password. If needed, add `--region EU` or `--region US`.
Use the diagnostic scan below to find device addresses and serial numbers.
Type 7 authentication also needs an external lookup table from
[ha-ef-ble](https://github.com/rabits/ha-ef-ble). From the bmspy repository root, download it:
```bash
curl --fail --location --output bmspy/ecoflow_keydata.py \
https://raw.githubusercontent.com/rabits/ha-ef-ble/main/custom_components/ef_ble/eflib/keydata.py
```
This file is ignored by Git and excluded from packages. Type 1 devices do not need it.
#### Dump both Rivers
Device arguments use `name@ADDRESS/SERIAL/USERID`. Replace the placeholders below
with each device's Bluetooth address, serial number, and your account user ID:
```bash
uv run --extra ecoflow-bt bmspy-server --socket /tmp/bmspy-rivers.sock \
-d 'river2@RIVER2_MAC/RIVER2_SERIAL/YOUR_USER_ID' \
-d 'river3@RIVER3_MAC/RIVER3_SERIAL/YOUR_USER_ID'
```
Leave the server running. In another terminal, print both snapshots:
```bash
uv run bmspy --socket /tmp/bmspy-rivers.sock
```
Omit `--ups` for all devices. Add `--ups river2` to select one. `--ups all` means a
device named `all`, not all devices. To include a serial BMS, add a server argument
such as `-d network:/dev/ttyUSB0`.
The server reads each device in its own background thread, so a slow or offline
device does not delay the other devices or the clients. It reads one Bluetooth
device at a time and waits 60 seconds after each Bluetooth read, including a
failed one. It reads a serial BMS every 5 seconds and retries a failed serial
read after 1 second. Unavailable devices are omitted from the response. Only the
first request after startup waits, until each device completes one read attempt.
The server answers each client in its own thread, so a slow client does not
delay the other clients. It closes a connection that stays silent for 5 seconds.
The server holds a Bluetooth connection only during a read, and disconnects
after each one. When the server stops, it ends a read in progress and
disconnects from that device before it exits. It waits at most 20 seconds for
the disconnect.
Each Bluetooth read listens for 10 seconds. If a required report is still
missing, the read continues until the report arrives, for at most 30 seconds in
total. A read that reaches this limit returns a partial snapshot. Its
`ef_missing_measurements` field names the missing reports, and the server log
shows one line each time that list changes.
#### Diagnostics
Stop the server before connecting directly. Scan and collect reports from both Rivers:
```bash
uv run --extra ecoflow-bt python -u tools/ecoflow-bt.py --user-id YOUR_USER_ID --scan --timeout 30
```
Add `--watch` to print every packet until Ctrl+C. Watch mode ignores the telemetry
timeout, but stops on a lost connection. Its payloads are already decrypted.
Keep diagnostic output private because it can contain device and account identifiers.
The scan detects encryption automatically. To select one discovered device,
add `--device ADDRESS`. Without scanning, replace `--scan` with
`--device ADDRESS --serial-number SERIAL`. Direct connections default to Type 7.
Session keys stay internal. The script has no encryption override or key-export option.
River 2 collection waits for three valid core reports, not optional inverter or
solar reports. River 3 waits for 53 baseline fields across several reports.
These checks do not guarantee every possible field. A timeout reports incomplete
collection. Use `--watch` to receive later reports.
Both models report charge, temperature, power, and time estimates through separate
decoders. Missing measurements stay absent. See the
[River 2 measurement reference](docs/ecoflow-river2-fields.md) and
[River 3 field reference](docs/ecoflow-river3-fields.md) for units, decoding rules,
and known limits.
Client snapshots also include reported protocol fields under `ef_river2_*` and
`ef_river3_display_*`, alongside the shared measurements and raw packets.
Partial reports retain earlier field values. Nested River 3 groups use JSON text
in their `info` fields. The references above describe names, units, and limits.
### Running via systemd
Before you start a service, install its dependencies in the service's working
directory with `uv sync --locked --no-dev`. Add `--extra ecoflow-bt` for Bluetooth,
`--extra influxdb` for InfluxDB, or both.
The service files use `uv run --no-sync`. With this option, a service does not
change the shared environment at startup. If the `uv` executable is not on
systemd's search path, set it to an absolute path.
Copy `bmspy-server.service` to `/etc/systemd/system`, adjust `WorkingDirectory` and the `ExecStart` line as needed, then enable and start it:
cp bmspy-server.service /etc/systemd/system
$EDITOR /etc/systemd/system/bmspy-server.service
systemctl daemon-reload
systemctl enable bmspy-server
systemctl start bmspy-server
## Running a client
To print a summary of all connected devices:
uv run bmspy
To show only a specific device:
uv run bmspy --ups network
To push data for all devices to InfluxDB (each measurement is tagged `ups=<name>`):
uv run --extra influxdb bmspy-influxdb --url https://influx.example.com --org myorg --token mytoken
To push data for a single device only:
uv run --extra influxdb bmspy-influxdb --ups network --url ...
InfluxDB connection details can also be supplied via environment variables (`INFLUXDB_V2_URL`, `INFLUXDB_V2_ORG`, `INFLUXDB_V2_TOKEN`) instead of command-line flags.
## Development
Install all extras and run the tests:
```bash
uv sync --locked --all-extras
uv run --locked --all-extras pytest
```
To build a source archive and a wheel, which is an installable Python package:
```bash
uv build
```
`uv.lock` records the resolved dependency versions. After you change dependencies
in `pyproject.toml`, run `uv lock` and include the updated lock file with the change.