Skip to main content

Python: Unofficial async client for Bluetti power stations over Modbus

PyPI Version Python Versions License Build Status Open in Dev Containers

Asynchronous Python client for Bluetti power stations over their local Modbus TCP interface.

About

This package reads Bluetti power stations over Modbus, using the register maps Bluetti documents for its Modbus TCP slave implementation. It is built on top of modbus-connection, a backend-neutral async Modbus toolkit: the library does not create or own a connection itself. The caller opens a ModbusConnection and hands over a ModbusUnit; a site with several devices creates one device object per unit, all sharing a single connection.

Because the caller owns the connection, the transport is up to you - Modbus TCP over Ethernet or WiFi is the common path, but anything that hands this library a ModbusUnit will do. This is the integration surface used to embed the library into another application, such as the bluetti_modbus Home Assistant integration (currently in review at home-assistant/core#180602).

This library is primarily read-only - it decodes what a device reports. A small, explicit set of fields bluetti-registers' schema marks writeable (currently: Balco260's 3 control switches and 2 battery SOC thresholds - not yet EP2000, which is still spec-derived rather than field-tested) also support await device.write(field_name, value), validated against the schema's own bounds (via probatio) before anything reaches the device. Everything else stays read-only.

Supported out of the box:

  • Balco260: battery voltage/current/SoC/SoH/cycle count, per-string PV, grid import/export, AC output, inverter status/fault/warning, and more
  • EP2000: the same Balco260 register set plus a rated-capacity and EMS/grid-export control block Balco260 doesn't have - sourced from BLUETTI's own official register spec, not yet verified against real EP2000 hardware (see bluetti-registers for the provenance of every field)
  • S Meter: Bluetti's AC meter/CT accessory (register map decoded, but not yet verified against real hardware)

EP2000 support was pulled for a while pending confirmation that it exposes Modbus TCP at all (see bluetti-official/bluetti-home-assistant#125) and re-added once BLUETTI published an official register spec confirming it does - the original report's closed port 502 on one specific unit is still unexplained, so treat this device as spec-derived rather than field-tested.

Field names, units, and register addresses come from bluetti-registers, not from hand-written tables in this repository - devices/balco260.py is generated from it by import.py, and a scheduled workflow re-runs that generator weekly and commits any diff, so main never silently drifts from what bluetti-registers currently documents.

Enabling Modbus TCP on your device

Modbus TCP is off by default on Bluetti power stations that support it - enable it in the device's own web interface first, then point this library at its IP address. See the official bluetti-modbus-tcp-slave documentation for the exact steps for your model; they vary enough between devices that this README won't guess at them.

Installation

pip install bluetti-modbus

Installing bluetti-modbus alone only pulls in modbus-connection's backend-neutral interface - enough to use the device classes directly against a ModbusUnit you already have. The bluetti-modread CLI, and the examples below, need a concrete backend, installed via the cli extra (currently pymodbus):

pip install "bluetti-modbus[cli]"

bluetti-modread also accepts --backend tmodbus (pip install "bluetti-modbus[cli-tmodbus]" first) - an active trial evaluating a possible future migration, not yet used by either HA integration. See CONTRIBUTING.md for why.

Usage

The consumer owns the connection and hands the library a unit:

import asyncio

from modbus_connection.pymodbus import connect_tcp

from bluetti_modbus_lib import BluettiModbusConnectionError, get_device


async def main() -> None:
    connection = await connect_tcp("10.2.1.60", port=502)
    try:
        unit = connection.for_unit(1)
        device = get_device("balco260", unit)
        if device is None:
            return

        try:
            await device.async_update_with_retry()
        except BluettiModbusConnectionError as err:
            print("Could not read the device:", err)
            return

        print(device.values["b_soc"], "%")
        print(device.values["b_v"], "V")
        print(device.values["d_inverter_status"])
    finally:
        await connection.close()


asyncio.run(main())

There is no self-describing header to detect the model from, unlike some Modbus devices - get_device() takes the model as a plain string ("balco260", "ep2000", or "smeter"); the caller has to already know which one it's talking to. async_update_with_retry() is the entry point most callers want: it retries once on a transient acknowledge/busy response (codes 5/6), which Bluetti devices return in practice on registers that otherwise read fine. Call async_update() directly instead if you want that first failure to raise immediately. Either way, a communication failure raises BluettiModbusConnectionError (also a modbus_connection.ModbusError, for code that already catches that directly) - except for a transient busy response, which async_update_with_retry decides whether to retry rather than wrapping. Decoded values land on device.values, a plain dict[str, Any] keyed by field name; field_names() and get_field() expose the field metadata (address, type, scale, unit, whether it's writable) behind each key, deliberately limited to what's true at the protocol level - no Home Assistant concepts like entity category or device class live here, since those describe UI presentation, not the register.

Everything above (get_device, the device classes, BluettiModbusError, BluettiModbusConnectionError, BluettiModbusClient, the inverter enums) is importable directly from bluetti_modbus_lib, not from the deeper module paths that define them.

CLI

The optional CLI reads a device straight from the terminal - useful for testing, not something another application should build on (see Architecture below).

bluetti-modread -c 10.2.1.60 -p 502 -t balco260

Example output, captured from a real Balco260 (truncated - bluetti-modread prints one line per field):

d_num_inverters: 1
ac_o_p_total: 84 W
pv_i_p_total: 0 W
ac_o_e_total: 64.7 kWh
d_inverter_status: InverterStatus.GridConnectedOperation
g_i_f: 50.0 Hz
b_v: 27.1 V
b_soc: 100 %
b_cycle_count: 8
b_t_avg: 0 °C
b_i_e: 23420 Wh

Note the two energy fields above: most cumulative energy fields (ac_o_e_total, etc.) are reported in kWh, but the battery charge/discharge ones (b_i_e, b_o_e) are in Wh - both correct as reported by the device, just worth knowing if you're comparing values across fields. Field names follow the naming convention documented in bluetti-registers.

Architecture

Two different things in this library talk Modbus, for two different audiences:

  • Balco260, EP2000, and SMeter (bluetti_modbus_lib.devices) are the integration surface: each takes a ModbusUnit supplied by the caller, built from whichever backend and connection the caller already manages. This is what an application - a Home Assistant integration, for example - should build on.
  • BluettiModbusClient (bluetti_modbus_lib.modbus.client) is different: it owns and manages its own connection. It exists for the bluetti-modread CLI above and standalone/manual use, not as something another application should depend on - doing so would open a second, competing connection to the device instead of sharing one.

Relationship to Patrick762's bluetti-modbus-lib

This repository started as a fork of Patrick762/bluetti-modbus-lib and has since diverged significantly (packaging, testing, retry handling, device coverage). Patrick762 is still actively maintaining his own version independently and was asked directly whether he'd like to fold this work back into his project or join bluetti-community - he's not in a position to commit the time to that right now, which is completely fine.

Since the PyPI name bluetti-modbus-lib is his and still actively used, this project is published on PyPI under a different name, bluetti-modbus, to avoid any ambiguity between the two. The GitHub repository itself keeps its original name.

Changelog & releases

This repository keeps a change log using GitHub's releases functionality. Publishing a release triggers the PyPI publish workflow directly (via Trusted Publishing, no stored token), setting the package version from the release tag.

Contributing

Contributions are welcome. See CONTRIBUTING.md for how to get started.

Setting up a development environment

The easiest way to start is by opening a Codespace here on GitHub, or by using the Dev Container feature of Visual Studio Code - either installs Python 3.13, the cli extra, and every dev tool below automatically, no local setup required.

Open in Dev Containers

To set it up manually instead: this project uses a plain venv + pip workflow - no Poetry, no Node tooling required. You need at least:

  • Python 3.13+
python -m venv .venv
source .venv/bin/activate
pip install -e ".[cli]"

As this repository uses pre-commit, changes are linted and formatted on every commit once you've run pre-commit install (the Dev Container does this for you automatically). script/run_checks.sh installs whatever's still missing (ruff, mypy, pytest) and runs all checks and tests manually, the same way CI does - formatting, ruff, mypy --strict, and the test suite with 100% coverage required:

script/run_checks.sh

To run just the Python tests:

pytest

script/format_code.sh applies ruff's safe autofixes and formats the tree.

Authors & contributors

The original author of bluetti-modbus-lib is Patrick762. This fork is maintained by bluetti-community.

For a full list of all authors and contributors, check the contributor's page.

Sponsoring

If you want to support this project, you can sponsor Patrick762 on GitHub, the original author.

Disclaimer

This project is an independent, community-driven effort. It is not affiliated with, endorsed by, or supported by Bluetti (PowerOak). All product names, trademarks, and registered trademarks are property of their respective owners.

The register map is based on Bluetti's own published bluetti-modbus-tcp-slave documentation and the bluetti-registers project. This work is done for interoperability purposes.

Use this software at your own risk. This library is provided without any warranty or support by Bluetti, and the authors are not responsible for any problems it may cause.

License

MIT License

Copyright (c) 2026 Patrick762

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Release files for bluetti-modbus 0.3.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bluetti-modbus 0.3.4
File Size Uploaded
bluetti_modbus-0.3.4.tar.gz 22.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for bluetti-modbus 0.3.4
File Interpreter ABI Platform
bluetti_modbus-0.3.4-py3-none-any.whl Python 3 none any Details

Total release size: 46.2 kB

Release files / bluetti_modbus-0.3.4.tar.gz

Download URL bluetti_modbus-0.3.4.tar.gz
Size 22.7 kB
Tags Source
SHA-256 checksum
How to use checksums
5990ec2c7e697d4031d9e8b57867b1be012883ea963059db6bfdf5a9f0135bdc
BLAKE2b-256 checksum
How to use checksums
6e278447b7cf0b1caf4716b724bb99245d88ecbf30478914dd05f04fc3f789b1
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 3, 2026.

Transparency log

Release files / bluetti_modbus-0.3.4-py3-none-any.whl

Download URL bluetti_modbus-0.3.4-py3-none-any.whl
Size 23.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
78311158fd421d055208e940214c1def73a891ec0d17be4b542825f5e66dd424
BLAKE2b-256 checksum
How to use checksums
7a09874f7c9e2ac1dcdcb2915ef48fb13df989ea469e3dc119955ce50e772b1a
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 3, 2026.

Transparency log

Release history Release notifications | RSS feed

0.34.0

2 release files

0.33.0

2 release files

0.32.0

2 release files

0.31.0

2 release files

0.30.4

2 release files

0.30.3

2 release files

0.30.2

2 release files

0.30.1

2 release files

0.30.0

2 release files

0.29.0

2 release files

0.28.1

2 release files

0.28.0

2 release files

0.27.2

2 release files

0.27.1

2 release files

0.27.0

2 release files

0.26.0

2 release files

0.25.1

2 release files

0.25.0

2 release files

0.24.0

2 release files

0.23.0

2 release files

0.22.1

2 release files

0.22.0

2 release files

0.21.0

2 release files

0.20.0

2 release files

0.19.5

2 release files

0.19.4

2 release files

0.19.3

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.5

2 release files

This release

0.3.4 This release

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page