Skip to content

About

Python implementation of the EVSEMaster app

Resources

Stars

0 stars

Watchers

1 watching

Forks

 
 

Latest commit

 

History

40 Commits

Folders and files

Repository files navigation

evsemaster

PyPI Python CI License: MIT

Unofficial Async Python client for EVSE chargers that speak the EVSEMaster app's UDP protocol, as used by Besen, Telestar, evseODM, Morec, Deltacom and other rebrands of the same hardware.

Using Home Assistant? You probably want the EVSEMaster integration instead.
It is built on this library and installs through HACS: sensors, start/stop, scheduled sessions and automatic discovery, with no code to write.

This library is the plumbing underneath it: one UDP socket, several chargers, parsed events. It is deliberately smaller than the original TypeScript project it is based on, emproto by johnwoo-nl.

Compatible devices

Confirmed by users of the Home Assistant integration:

Chargers differ in small ways the protocol cannot be asked about, so per-model quirks live in capabilities.py. An unrecognised charger gets the permissive default and normally just works. Report a working charger here to get it listed.

Installation

Published on PyPI as evsemaster; requires Python 3.14 or newer.

pip install evsemaster

Usage

One EvseListener owns the UDP socket; every charger is an EvseDevice added to it. Incoming packets are routed by the device serial in the packet header, so several chargers share the one socket.

import asyncio
from evsemaster import EvseListener


def on_event(event_type, data):
    print(f"Event: {event_type}, Data: {data}")


def on_discovery(discovered):
    print(f"Found an unconfigured charger: {discovered}")


async def main():
    listener = EvseListener(on_discovery=on_discovery)
    await listener.start()

    evse = await listener.async_add_device(
        host="10.0.0.1",  # IP address of the EVSE
        password="123456",  # 6 digit password of the EVSE
        on_event=on_event,  # called with every parsed update
    )
    if await evse.login():
        await evse.request_status()
        await evse.start_charging()
        await evse.stop_charging()

    await listener.stop()


asyncio.run(main())

Updates arrive through on_event(event_type, data), where event_type is the model name: "EvseStatus", "ChargingStatus", "EvseDeviceInfo", "WeeklyPlan" or "ChargeRecord".

A ChargeRecord arrives when a session ends, and the library acknowledges it so the charger stops rebroadcasting it. The EVSEMaster app lists only records it acknowledged itself, so it won't show sessions this library saw first. await evse.read_history() reads the charger's own store of its last 300 sessions. The charger does not say which of its 300 slots holds the newest session, so every read asks for all of them, one at a time, which takes a few minutes. It returns a ChargeHistory: records keyed by slot, and unread_slots, the slots that never answered after retries. A session in an unread slot is missing from records.

Scheduling a session

from datetime import timedelta
from evsemaster import now_aware

await evse.start_charging(
    max_amps=16,
    start_date=now_aware() + timedelta(hours=2),  # must be timezone aware
    duration_minutes=180,
)

Chargers interpret every timestamp as Asia/Shanghai local time, and some firmware has a clock that drifts by weeks. The library converts and compensates for both, so pass normal aware datetimes and read evse.time_delta if you want to see the skew it corrected.

Networking

Chargers broadcast their presence on UDP port 28376, which is how the library learns a charger's send port, follows it across DHCP changes and logs back in after a session drops. Those broadcasts have to reach your host: a separate VLAN or Docker bridge networking blocks them.

listener.probe() broadcasts a password-less login request for chargers that stay quiet, so discovery works without a real password crossing the network.

A device counts as logged in while the charger keeps talking to it: evse.is_logged_in needs a successful login plus a packet other than an announcement within the last 120 seconds (evse.last_alive).

Implemented

  • Multiple chargers on one UDP socket, routed by device serial
  • Discovery of chargers on the network, including ones that must be asked
  • Device info, status and charging status as parsed pydantic models
  • Start/stop charging, including reservations with a start time and a duration
  • Get/set nickname
  • Get/set current limit, refused on models that silently ignore it mid-charge
  • Get/set the display language
  • Get/set the weekly plan, the charger's own recurring schedule
  • Fault names decoded from the status error bits
  • Finished-session records, acknowledged as they arrive, and the charger's session history
  • Device time, with correction for on-device clock drift

Not planned

  • Home Assistant or MQTT integration, see evsemaster-homeassistant
  • Bluetooth setup (getting the charger onto wifi). The EVSEMaster app is unpleasant, but you only need it once.

Development

poetry install
poetry run poe check      # ruff check and pytest

evsemaster.testing.FakeEvse is a charger that speaks enough of the protocol to drive a real client over loopback. It is public API, so downstream projects can use it in their own tests.

For poking at real hardware there is an interactive script that takes one or more chargers:

poetry run python test.py 10.0.0.1:123456 10.0.0.2:654321

Shortcuts: status, start [amps] [YYYY-MM-DDTHH:MM:SS] [minutes], stop, discover, use <n>, all.

Credits

The protocol was reverse engineered by @johnwoo-nl in emproto. Packet structures, command codes and the communication patterns all come from that work; this library is a Python take on it.

About

Python implementation of the EVSEMaster app

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Contributors

Languages