Skip to main content

easee-ble

Local Bluetooth control for Easee EV chargers. Talk to the charger directly with the PIN printed on the unit.

pip install easee-ble

Requires Python 3.11+. For Home Assistant, use ha-easee-ble, which is built on this.

Quick start

import asyncio
from bleak import BleakScanner
from easee_ble import EaseeCharger, PhaseMode

async def main():
    device = await BleakScanner.find_device_by_address("AA:BB:CC:DD:EE:FF")
    charger = EaseeCharger(device, pin="1234", serial="EMX00000")

    await charger.connect()                # handshake; the link is held open
    print(await charger.poll())            # {'chargerOpMode': 3, 'totalPower': 2.69, ...}

    await charger.perform(lambda s: s.set_charger_enabled(True))
    await charger.perform(lambda s: s.set_phase_mode(PhaseMode.AUTO))

    await charger.disconnect()

asyncio.run(main())

You need two things from the charger: the PIN printed on the unit, and its serial (EMXXXXXX, also on the unit and in the Easee app).

Finding your charger

Scan by service UUID if you do not know the address:

from easee_ble import SERVICE_UUID

device = await BleakScanner.find_device_by_filter(
    lambda d, adv: SERVICE_UUID in adv.service_uuids
)

Chargers usually advertise only intermittently. To get one to show up, either set Bluetooth to always-on in the Easee app, or open a window with a long press of the charger's touch button.

Reading

poll() merges State and Config into one dict of named fields:

data = await charger.poll()
data["chargerOpMode"]        # 3
data["totalPower"]           # 2.69
data["maxChargerCurrent"]    # 16
charger.unknown              # fields we have no name for yet, by number

Two fields belong together. chargerOpMode describes the car and cable - it still reads AWAITING_START for a charger that has been switched off. reasonForNoCurrent says why no current flows, and reads 53 (charger disabled) in exactly that case. Show both:

from easee_ble import charger_op_mode, reason_for_no_current

charger_op_mode(data["chargerOpMode"])                # ChargerOpMode.CHARGING
reason_for_no_current(data.get("reasonForNoCurrent")) # 'Charger disabled'

Both return None on a value this library has not seen, where ChargerOpMode() and NetworkStatus() raise. The descriptions are display text; key on reason_for_no_current_slug() if you need something stable.

Fields at their default value are absent from the wire, not zero. poll() already fills a curated set back in as 0, so treat a missing field as unknown rather than surprising.

Commands

Every command goes through perform(), which builds the request and checks the answer:

await charger.perform(lambda s: s.set_charger_enabled(True))
await charger.perform(lambda s: s.set_max_charger_current(16))
await charger.perform(lambda s: s.set_dynamic_charger_current(10))
await charger.perform(lambda s: s.set_circuit_max_current(20))          # or (p1, p2, p3)
await charger.perform(lambda s: s.set_offline_max_circuit_current(10))
await charger.perform(lambda s: s.set_phase_mode(PhaseMode.LOCKED_3_PHASE))
await charger.perform(lambda s: s.set_led_brightness(75))               # 0-100
await charger.perform(lambda s: s.set_cable_locked(True))
await charger.perform(lambda s: s.set_access_control(True))

PhaseMode is LOCKED_1_PHASE, AUTO or LOCKED_3_PHASE.

RFID / account keys

from easee_ble import command_payload

reply = await charger.perform(lambda s: s.list_user_tokens())
keys = command_payload(reply)
await charger.perform(lambda s: s.get_user_token(slot, name))
await charger.perform(lambda s: s.set_user_token(slot, name, token))

Other reads

Besides poll(), individual frames are available through perform():

await charger.perform(lambda s: s.poll_state())
await charger.perform(lambda s: s.poll_config())
await charger.perform(lambda s: s.poll_structure())
await charger.perform(lambda s: s.poll_debug())

A reply only arrives on a subscribed channel, and the last two are not subscribed to by default. Ask for them at connect - EaseeCharger(..., channels=DEFAULT_CHANNELS | {Channel.DEBUG}) - or the call raises. poll() takes the same argument to read them alongside Config and State.

Errors

from easee_ble import EaseeCommandRefused, EaseeConnectionError, JPakeError

try:
    await charger.connect()
    await charger.perform(lambda s: s.set_max_charger_current(32))
except JPakeError:            # almost always the wrong PIN
    ...
except EaseeCommandRefused:   # the charger answered, and said no
    ...
except EaseeConnectionError:  # connecting or talking to it failed
    ...

A reply is not an acknowledgement - the charger answers a command it refuses just as promptly as one it accepts. perform() checks for you and raises EaseeCommandRefused; anything else, command_accepted() and command_refusal() check by hand.

Connection notes

  • The connection is long-lived: connect once and keep polling. Connecting is the expensive, failure-prone part.
  • Not safe for concurrent use. One request may be in flight at a time - serialise with a lock if several tasks share a charger.
  • Pass on_disconnect= to be told the moment the link drops rather than at your next poll; check charger.connected before using it.
  • The charger has a single connection slot. Always disconnect() when done.
charger = EaseeCharger(device, pin="1234", serial="EMX00000",
                       on_disconnect=lambda c: print("link lost"))

Without bleak

Session is sans-io: it builds requests and parses replies and does no I/O, so you can drive it over any transport.

from easee_ble import Session

s = Session(pin="1234", serial="EMX00000")
req = s.start_handshake()          # write req.data to req.channel, feed the reply back
req = s.read_round_one(reply)
s.read_round_two(reply)            # s.established is now True
frame = s.parse(req.channel, reply)

Development

pip install -e ".[dev]"
pytest

Tests run against captured bytes from a real charger, so they need no hardware.

Notes

Unofficial. Reverse-engineered. Not affiliated with or endorsed by Easee. No warranty, changing charger settings is at your own risk. Barely tested.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

easee_ble-0.1.0.tar.gz (42.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

easee_ble-0.1.0-py3-none-any.whl (29.6 kB view details)

Uploaded Python 3

File details

Details for the file easee_ble-0.1.0.tar.gz.

File metadata

  • Download URL: easee_ble-0.1.0.tar.gz
  • Upload date:
  • Size: 42.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for easee_ble-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f960eb2ae69e3b8be6fd64e9f6866431cd247c66a608c7293aafc85b9362ab34
MD5 df700e6f1e69c54c964df52cfc24309c
BLAKE2b-256 e56ad56b7dd6ed99dec7c4e9076c99a80f889b2a766196cd201bfd1e38ea4932

See more details on using hashes here.

Provenance

The following attestation bundles were made for easee_ble-0.1.0.tar.gz:

Publisher: publish.yml on parrel/easee-ble

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file easee_ble-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: easee_ble-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 29.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for easee_ble-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0742eb2a2e96275433a2d4a90b142e51dbe4cbf5db922832cb2d1d009cca6227
MD5 debe94910ba6683ee5470e97b0164db0
BLAKE2b-256 e2988b7759e3b838b0057b7dbb3358253f521961d485fb152c66a566eaadd1f0

See more details on using hashes here.

Provenance

The following attestation bundles were made for easee_ble-0.1.0-py3-none-any.whl:

Publisher: publish.yml on parrel/easee-ble

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

1.0.0

2 files

0.2.0

2 files

0.1.1

2 files

This release

0.1.0 This release

2 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