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.1.tar.gz (43.1 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.1-py3-none-any.whl (29.9 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: easee_ble-0.1.1.tar.gz
  • Upload date:
  • Size: 43.1 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.1.tar.gz
Algorithm Hash digest
SHA256 04118829a9f3e23c6ec22d160716b1ddf24682d8848e24f42c00ad079096fa20
MD5 30bd43d66aca61626f129121870242d5
BLAKE2b-256 c69a984dc47c25c274a11c76fbb4ab922a6306c10a2ad55b600a76419bf223db

See more details on using hashes here.

Provenance

The following attestation bundles were made for easee_ble-0.1.1.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.1-py3-none-any.whl.

File metadata

  • Download URL: easee_ble-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 29.9 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 93c9caedecdc31e85273044aebcafcd38057c07bef7e551dd7cdfca0960fd443
MD5 5fcd6ba426b9d9748cfb545f7231915e
BLAKE2b-256 a2be6ef06fa84bf2450fcd52b0c67e1ce8073543e7b76b96e9a6e42154eb5c6d

See more details on using hashes here.

Provenance

The following attestation bundles were made for easee_ble-0.1.1-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

This release

0.1.1 This release

2 files

0.1.0

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