Skip to content

Persistent BLE connection for instant writes and original app compatibility #60

Open
defire04 wants to merge 5 commits intoPatrick762:mainfrom
defire04:feature-persistent-ble-connection
Open

Persistent BLE connection for instant writes and original app compatibility #60
defire04 wants to merge 5 commits intoPatrick762:mainfrom
defire04:feature-persistent-ble-connection

Conversation

@defire04
Copy link
Copy Markdown

@defire04 defire04 commented Mar 21, 2026

Problem

Building on #59, I tested switches and selects on my AC180P. The feature worked, but every toggle took 5–14 seconds:

Step Time
establish_connection 0–9 s (unpredictable, main bottleneck)
ECDH handshake ~1.3 s
GATT write ~0.03 s
Disconnect ~2.25 s
asyncio.sleep(2) 2 s

The root cause: DeviceWriter opened a fresh BLE connection and ran the full ECDH handshake for every single write command, then disconnected.

Holding the connection open was the obvious fix — but it revealed a second problem: the device supports only one BLE connection at a time, so the official Bluetti app could not connect while Home Assistant held the link.

What changed

bluetti_bt_lib/bluetooth/device_connection.py (new)

A new DeviceConnection class that owns a single BleakClient and a BluettiEncryption session shared between DeviceReader and DeviceWriter.

The connection lifecycle:

  1. connect() — scans for the device, establishes the BLE link
  2. Subscribe to NOTIFY_UUID with a single shared handler
  3. Perform ECDH handshake (CHALLENGE → CHALLENGE_ACCEPTED → PEER_PUBKEY → PUBKEY_ACCEPTED); completion signaled via asyncio.Event
  4. connect() returns True — reader/writer can now use the shared session
  5. disconnect() — unsubscribes, resets encryption state, closes the link

The single notification handler routes messages internally:

Message type Handled by
CHALLENGE, CHALLENGE_ACCEPTED _handle_pre_key_message() (during connect())
PEER_PUBKEY, PUBKEY_ACCEPTED _handle_encrypted_key_message() (during connect())
Modbus responses _dispatch_data() → forwarded via set_data_callback()

bluetti_bt_lib/bluetooth/device_writer.py

Added connection: DeviceConnection | None = None parameter. When provided, the writer skips establish_connection, skips the ECDH handshake (already done), uses connection.encryption for AES encryption, and does not disconnect on cleanup — the shared connection stays alive.

When connection=None the behaviour is identical to before.

bluetti_bt_lib/bluetooth/device_reader.py

Added connection: DeviceConnection | None = None parameter. When provided:

  • Uses the shared BleakClient instead of opening its own
  • Registers a data callback via connection.set_data_callback() instead of subscribing to NOTIFY_UUID
  • Skips the handshake wait (already complete)
  • After each read cycle, clears the data callback and schedules a deferred disconnect using keep_alive_seconds from DeviceReaderConfig (default 0). This keeps the BLE session alive briefly so writes arriving within that window are instant, then frees the slot so the original Bluetti app can connect

When connection=None the behaviour is identical to before.

bluetti_bt_lib/__init__.py / bluetti_bt_lib/bluetooth/__init__.py

Added DeviceConnection to the top-level package exports.

Write latency after this change

Scenario Time
Write within keep_alive window ~0.03 s
First write (fresh connection) ~1.3 s
After connection drop (reconnect) ~1.3 s

Tested on

Bluetti AC180P via ESPHome BLE proxy
AC output, DC output, Power Lifting, Charging Mode — all confirmed working

The HA integration side (coordinator wiring, shared lock, write_pending guard, deferred disconnect) is in a test branch:
defire04/hassio-bluetti-bt @ test-encrypted-writes-and-persistent-ble-connection

Notes

This PR builds on #59 — the encryption handshake implementation there is unchanged; DeviceConnection reuses it and lifts it out of DeviceWriter so both reader and writer can share the session.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant