- Python 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
bmspy-beszel connects to a Beszel hub over a WebSocket and uses the protocol of the official agent. It verifies the signature of the hub before it sends data. Beszel has no custom metrics, so the agent sends only the charge, the charge state, and the temperatures. |
||
| bmspy | ||
| docs | ||
| tests | ||
| tools | ||
| .gitignore | ||
| bmspy-beszel@.service | ||
| bmspy-influxdb.service | ||
| bmspy-server.service | ||
| bmspy-ups.service | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
bmspy
bmspy is a tool to get information from battery management systems and UPS devices. It supports:
- Xiaoxiang-type 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, a Prometheus exporter, or a Beszel hub.
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 and use Python 3.12 or later, below Python 4. Then create the local environment:
git clone https://git.treehouse.org.za/tim/bmspy
cd bmspy
uv sync --locked
To install with EcoFlow Bluetooth LE support:
uv sync --locked --extra ecoflow-bt
To install with InfluxDB and Prometheus support:
uv sync --locked --extra influxdb --extra prometheus
To install with Beszel support:
uv sync --locked --extra beszel
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:
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. From the bmspy repository root, download it:
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:
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:
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 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 reads one Bluetooth device at a time and waits 60 seconds after each
good Bluetooth read. To change that wait, add --bluetooth-interval SECONDS to
the server command. During the wait, clients receive the last read. After a
failed Bluetooth read, the server tries again after 60 seconds. An interval
below 60 seconds also applies after a failed read.
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:
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 and River 3 field reference 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, --extra beszel for Beszel, or more than one.
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.
Beszel
bmspy-beszel connects to a Beszel hub in the role of a Beszel agent. The
connection goes out from the bmspy host over a WebSocket, so the hub does not
need access to that host. The hub requests data every minute.
Follow these steps to connect a hub:
- In the hub, add a system. Enter any host name and port, because bmspy does not use them.
- Copy the token and the public key from the same dialog.
- Start the agent on the host that runs
bmspy-server:
uv run --extra beszel bmspy-beszel --url https://beszel.example.com --token mytoken --key "ssh-ed25519 AAAA..."
The HUB_URL, TOKEN, and KEY environment variables can supply these values
instead. The official agent uses the same names. bmspy --beszel also starts the
agent, and it reads its values only from these variables.
bmspy-beszel@.service runs one agent for one device. The instance name is the
device name. Follow these steps to install it:
- In the hub, create a universal token. When an agent connects for the first time, this token creates a system.
- Put
HUB_URL,TOKEN, andKEYin/etc/bmspy/beszel.env. Make that file readable by root only. - Copy
bmspy-beszel@.serviceto/etc/systemd/system. - Start one instance for each device:
systemctl daemon-reload
systemctl enable --now bmspy-beszel@network bmspy-beszel@desktop
Each system has the name of its device. Use the device names from the server configuration. If a name does not match, the server returns all devices, and that system shows all of them.
The agent verifies that the hub signed the token with the private key before it
sends any data. It accepts only an ssh-ed25519 key, which is the type that a
hub generates.
Beszel uses a set list of fields and no custom metrics, so the agent sends this data:
| bmspy measurement | Beszel field |
|---|---|
bms_capacity_charge_ratio |
Battery chart, with one line for each device |
bms_charge_is_charging, bms_charge_is_discharging |
Battery state |
bms_temperature_celsius |
Temperature chart, with the device name in each sensor name |
The agent does not send other measurements, for example voltage, current, or power. The CPU, memory, disk, and network values of the system are zero. To monitor the host also, run the official agent as a separate system.
The battery summary in the system list shows the device with the lowest charge. A Beszel low battery alert therefore applies to every device.
An agent without --ups reports all devices as one system. Each --ups value
gives a different fingerprint, which is the value that the hub uses to identify
an agent.
If a device has no data, the system stays up and the device has no line in the charts for that time. If the agent cannot read from the bmspy server, the hub shows the system as down.
Grafana
Run python tools/generate_grafana_ups.py to write grafana-ups.json. Import
that file for a three-column view of Network
(JBD), Desktop (River 2), and NAS (River 3). Queries use the network, desktop,
and nas tags, respectively. The top rows show the present state of each
device, the power source history, and the charge history. Select an InfluxDB
data source configured for Flux.
See the dashboard guide for setup and measurement limits.
Development
Install all extras and run the tests:
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:
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.