Skip to main content

Besen Python library

PyPI Python versions CI License

besen is an async Python client for Besen EV chargers over Bluetooth Low Energy. It provides connection management, PIN authentication, protocol parsing, typed state models, and charger commands.

Home Assistant users don't need to install it; the Besen integration does that automatically.

Installation

For standalone Python applications:

pip install besen

Python 3.12 or newer is required. The library has been verified with a Besen BS20. Other Besen chargers advertising as ACP#... and using the same BLE protocol may also work.

Basic usage

Applications provide the BLE device lookup function. This keeps discovery policy outside the library, so callers can use bleak, Home Assistant Bluetooth helpers, or another BLE stack integration.

Replace the example address and PIN with your charger's values. This example observes state updates for one minute; it does not start or stop charging. Disconnect other apps or bridges from the charger before running it.

import asyncio
import logging

from bleak import BleakScanner
from bleak.backends.device import BLEDevice

from besen import BesenClient, BesenData

ADDRESS = "AA:BB:CC:DD:EE:FF"
PIN = "123456"


async def main() -> None:
    logging.basicConfig(level=logging.INFO)
    logger = logging.getLogger("besen")

    device = await BleakScanner.find_device_by_address(ADDRESS, timeout=10.0)
    if device is None:
        raise RuntimeError("Charger was not found")

    def device_provider() -> BLEDevice | None:
        return device

    client = BesenClient(
        address=ADDRESS,
        pin=PIN,
        ble_device_provider=device_provider,
        logger=logger,
    )

    def handle_update(data: BesenData) -> None:
        print(
            "available=",
            data.available,
            "charging=",
            data.charge.charger_status,
            "power_w=",
            data.charge.power,
            "session_kwh=",
            data.charge.session_energy,
        )

    remove_listener = client.add_listener(handle_update)

    try:
        await client.async_start()
        await asyncio.sleep(60)
    finally:
        remove_listener()
        await client.async_stop()


asyncio.run(main())

Client API

Create one BesenClient per charger:

client = BesenClient(
    address="AA:BB:CC:DD:EE:FF",
    pin="123456",
    ble_device_provider=device_provider,
    logger=logger,
    advertised_name="ACP#Garage",
    sync_clock=True,
)

The BLE device provider is called before connection attempts and reconnects. It must return a connectable bleak.backends.device.BLEDevice or None when no connectable path is available.

Lifecycle and state

  • await client.async_start() connects, subscribes to notifications, and completes the charger login flow.
  • await client.async_stop() cancels background tasks and disconnects.
  • client.add_listener(callback) registers a synchronous state callback and returns a function that removes it.
  • client.state returns the latest BesenData snapshot.
  • client.is_connected reports whether the underlying BLE connection is open.

Control methods

These methods can change the charger or operate real charging hardware. Test them manually with appropriate supervision before using them in an automation.

  • await client.async_start_charging(amps=None, *, start=None, duration_minutes=None)
  • await client.async_stop_charging()
  • await client.async_set_charge_amps(amps)
  • await client.async_refresh_charge_amps()
  • await client.async_set_lcd_brightness(brightness)
  • await client.async_set_temperature_unit(unit)
  • await client.async_set_language(language)
  • await client.async_set_device_name(name)
  • await client.async_refresh_config()

Scheduled and time-limited charging

async_start_charging() starts a session immediately and without a time limit unless you pass one of these keyword arguments:

  • start: a timezone-aware datetime up to 24 hours ahead. The charger waits until then before it starts charging. The firmware calls this a reservation.
  • duration_minutes: an integer from 1 to 65534. The charger ends the session after that many minutes of charging.
from datetime import UTC, datetime, timedelta

await client.async_start_charging(
    amps=16,
    start=datetime.now(UTC) + timedelta(hours=2),
    duration_minutes=90,
)

An invalid start time or duration raises ValueError before anything is sent. The charger reports an accepted schedule back as BesenData.charge.scheduled_start and BesenData.charge.charging_time_limit.

The charger measures the start time against its own clock. The client sets that clock on every login and at most once an hour afterwards, so keep sync_clock=True when you schedule sessions.

State model

State updates are immutable dataclasses. Every listener receives a full BesenData snapshot.

Important fields:

  • BesenData.available: whether the latest BLE state is usable.
  • BesenData.authenticated: whether the PIN login flow completed.
  • BesenData.auth_failed: whether the charger explicitly rejected the PIN. Automatic reconnects stop until a new login is started; a Bluetooth outage or incomplete login does not set it.
  • BesenData.info: charger metadata such as serial, model, phases, firmware, and board revision.
  • BesenData.config: configuration values such as charge amps, device name, language, temperature unit, LCD brightness, and RSSI.
  • BesenData.charge: live charging state such as voltage, current, energy, temperature, plug state, output state, and charger status.
  • BesenData.last_command: last parsed command response.
  • BesenData.last_error: last connection, protocol, or command error string.

Important telemetry fields:

  • BesenData.charge.power: charger-reported charging power in watts.
  • BesenData.charge.total_energy: lifetime energy counter in kWh.
  • BesenData.charge.session_energy: energy delivered during the current or most recently completed charging session in kWh.
  • BesenData.charge.session_start and scheduled_start: timezone-aware UTC datetimes from the latest session report, or None when unset. The scheduled start is the requested start time, which the firmware calls a reservation.
  • BesenData.charge.session_duration: elapsed session time in seconds.
  • BesenData.charge.session_current_limit: current limit recorded for the session in amperes, or None when unset.
  • BesenData.charge.charging_time_limit: time after which the charger ends the session in minutes, or None when unset or unlimited.
  • BesenData.charge.inner_temp_c and BesenData.charge.outer_temp: temperatures in Celsius, or None when the charger reports an invalid value.
  • BesenData.charge.l1_voltage, l2_voltage, and l3_voltage: phase voltages in volts. L2 and L3 are populated only when the charger sends three-phase data.
  • BesenData.charge.l1_amperage, l2_amperage, and l3_amperage: phase currents in amperes. L2 and L3 are populated only when the charger sends three-phase data.

Exceptions

All library-specific errors inherit from BesenError.

  • CannotConnect: the charger could not be reached or login timed out.
  • NoConnectablePath: no active BLE path is available.
  • InvalidAuth: the charger rejected the configured PIN.
  • ProtocolError: malformed charger data was received.
  • CommandFailed: a charger command could not be sent, was invalid, or a start-charging request was rejected or could not be confirmed.

async_start_charging() waits up to 10 seconds for the charger response, including the Bluetooth write. A response with a charging or reservation error raises CommandFailed, for example when the charger is already charging or already holds a scheduled start. A successful response confirms the request, while actual charging state continues to arrive through notifications.

If the response times out, the charging outcome is unknown. The client disconnects and reconnects without automatically sending another start request. Cancellation after sending also retires that Bluetooth session so a late reply cannot be mistaken for the next request's response. Connection cleanup can extend the time before the method exits. Concurrent start requests wait their turn before the 10-second timeout begins. Other command methods retain their existing behavior.

Bluetooth notes

Besen chargers normally allow only one active BLE client connection. Stop other tools or apps that may already be connected to the charger before starting this client.

The client keeps one active BLE connection open, listens for notifications, replies to heartbeats, and schedules reconnects when notifications stop. The caller remains responsible for device discovery, adapter/proxy selection, and deciding when to start or stop the client.

Safety and attribution

This library is not a safety controller. Keep hardware protections, wiring, and current limits appropriate for the installation independently of the software.

The Python library is MIT-licensed, with protocol work based on slespersen/evseMQTT. See LICENSE and NOTICE.md.

Development

See CONTRIBUTING.md for the development setup, checks, and release process. Release notes are in CHANGELOG.md.

Metadata

Release files for besen 0.4.9

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

Source distribution (sdist)

Source distribution for besen 0.4.9
File Size Uploaded
besen-0.4.9.tar.gz 44.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for besen 0.4.9
File Interpreter ABI Platform
besen-0.4.9-py3-none-any.whl Python 3 none any Details

Total release size: 69.2 kB

Release files / besen-0.4.9.tar.gz

Download URL besen-0.4.9.tar.gz
Size 44.9 kB
Tags Source
SHA-256 checksum
How to use checksums
0b5c83b4cff16d608c2c1977ac241ff29acde210bed33adc954677ee9622789c
BLAKE2b-256 checksum
How to use checksums
fe7430a209ee69c21d26c602920bf42c5ed56034999d4b6de0d57f734c864915
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 Oct 1, 2026.

Transparency log

Release files / besen-0.4.9-py3-none-any.whl

Download URL besen-0.4.9-py3-none-any.whl
Size 24.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
855cafca7aac96382be89ac69d62d33cd85011a617ccdf2cca5aedeaacea2954
BLAKE2b-256 checksum
How to use checksums
4285ee8d9f4c192e6d4ab5cd7919e8d645f7a75de941b8305ca52d3f94b6dbf4
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 Oct 1, 2026.

Transparency log

Release history Release notifications | RSS feed

0.4.11

2 release files

This release

0.4.9 This release

2 release files

0.4.8

2 release files

0.4.7

2 release files

0.4.6

2 release files

0.4.5

2 release files

0.4.4

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.4

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

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