On-device OCPP 1.6J Charge Point external component for ESPHome, built on top of matth-x/MicroOcpp.
Lets any ESPHome-controlled EV charger register with a Central System (CSMS) — SteVe, CitrineOS, evcc, Monta, ev.energy, etc. — and expose itself as a standard OCPP Charge Point over WebSocket. No always-on Home Assistant needed for the OCPP link itself.
Working but not in active use. Tested end-to-end against
evcc and SteVe on a Quectel FC41D (BK7231N) inside a
reverse-engineered Rippleon ROC001 EV charger. BootNotification,
Heartbeat, MeterValues, StartTransaction / StopTransaction (incl. RFID
idTag flow), ChangeConfiguration, TriggerMessage, and RemoteStop /
RemoteStart all exercised on hardware and round-tripped successfully
with the CSMS.
The Rippleon deployment moved off OCPP and onto a Home Assistant ↔ evcc integration instead — the Rippleon MCU's own OCPP code (the bit we don't replace) has too many bugs to make the OCPP path the path of least resistance for that charger; see the esphome-rippleon-ev README for the full list. None of those issues are this component's fault — it correctly speaks OCPP 1.6J on its side.
That means this repo is no longer being exercised on a daily basis.
It compiles cleanly (CI runs esphome compile on every push) and the
last bench run was successful, but if you hit a regression please file
an issue rather than assume someone else is already on it. PRs welcome.
Compiles under LibreTiny (bk72xx); should compile on ESP32 / ESP8266
since MicroOcpp supports those upstream.
| Path | Contents |
|---|---|
components/ocpp/ |
The ESPHome external component. Import via external_components:. |
server/ |
Docker-compose kit for a local SteVe dev CSMS. |
example/ |
Reference YAML configs. |
cd server
docker-compose upSteVe web UI: http://localhost:8180/steve/manager/home. The init.sql seeds
a Charge Point identifier rippleon-charger-01 so you can point firmware at
ws://<docker-host>:8180/steve/websocket/CentralSystemService/rippleon-charger-01
without first registering it in the UI.
external_components:
- source:
type: git
url: https://github.com/RAR/esphome-ocpp-server
ref: main
refresh: always
components: [ocpp]
ocpp:
id: ocpp_cp
csms_url: ws://192.168.1.10:8887/my-charger
charge_point_id: my-charger
vendor: My Company
model: My Charger Model
firmware_version: "1.0.0"
phase: L1 # OCPP phase tag for V/I (single-phase: L1)
nominal_voltage: 240.0 # 120 US outlet / 230 EU 1ϕ / 240 US split / 400 EU 3ϕ L-L
phase_switching_supported: false # true only for EU 3-phase 1p3p switchable EVSEs
lock_offered_current_during_transaction: false
# true on EVSEs whose hardware can't change
# offered current mid-session — pins the
# Current.Offered measurand to its StartTx
# value so evcc stops logging "current
# mismatch" on attempted mid-session derates
# 3-phase EVSEs swap the `phase:` scalar above for a `phases:` list and
# use {l1,l2,l3} dicts under voltage / current in meter_values:
# phases: [L1, L2, L3]
# meter_values:
# voltage: {l1: v_l1, l2: v_l2, l3: v_l3}
# current: {l1: i_l1, l2: i_l2, l3: i_l3}
# power: total_power
# energy: total_energy
heartbeat_interval: 60s # pin Heartbeat (CSMSes often default to hours)
meter_value_sample_interval: 30s # pin MeterValueSampleInterval (≥30s for evcc)
stop_txn_sampled_data: # measurands shipped only at StopTransaction
- Energy.Active.Import.Register # distinct from the periodic feed —
- SoC # tighter list for billing transcripts
meter_values:
voltage: voltage_a_sensor # → Voltage (V)
current: current_a_sensor # → Current.Import (A)
power: power_sensor # → Power.Active.Import (kW input → W)
energy: charge_energy_sensor # → Energy.Active.Import.Register (kWh input → Wh)
current_offered: max_current_n # number::Number → Current.Offered (A)
# Power.Offered auto-computed from Voltage*Number
temperature: system_temp_sensor # → Temperature, Location=Body
soc: ev_soc_sensor # → SoC, Location=EV (HA-mirrored sensor)
frequency: grid_freq_sensor # → Frequency (Hz, HA-mirrored)
power_factor: grid_pf_sensor # → Power.Factor (HA-mirrored)
status_from: status_text_sensor
status_mapping: # local-state → OCPP-status
"Not Connected": Available
"Ready to Charge": Preparing
"Charging": Charging
"Stopping": Finishing
"Complete": Finishing
"Fault": Faulted
plugged_from: plugged_binary # authoritative cable-plug signal
soc_plugged_from: ev_connected_bs # EV-identity gate for SoC (optional)
on_remote_start: # fires on real StartTx (not just request)
- lambda: id(my_charger)->start();
on_remote_stop: # fires on real StopTx
- lambda: id(my_charger)->stop();
on_reset:
- lambda: id(my_charger)->reset();
on_charging_profile_change: # SmartCharging effective limit
- lambda: |-
if (current_limit_a == 0.0f || power_limit_w == 0.0f) {
id(my_charger)->stop();
} else if (current_limit_a > 0.0f) {
// forward to charger's max-current control
}
on_trigger_message: # CSMS-initiated diagnostic poll
- lambda: |-
ESP_LOGI("evcc", "TriggerMessage: %s connector=%d",
requested_message.c_str(), connector_id);
on_data_transfer: # CSMS-side vendor extension messages
- lambda: |-
ESP_LOGI("vendor", "DataTransfer: %s/%s data=%s",
vendor_id.c_str(), message_id.c_str(), data.c_str());
text_sensor:
- platform: ocpp
ocpp_id: ocpp_cp
connection_state: # disconnected / connecting / handshaking
name: "OCPP State" # / connected / ready / closing
# YAML-callable actions to start/stop transactions from any automation.
# Useful for Plug & Charge UX where you want to skip RFID auth — wire
# these to a binary_sensor edge on the charging-state signal.
binary_sensor:
- platform: template
id: is_charging
lambda: 'return id(some_evse_state).state == "CHARGING";'
on_press:
- ocpp.start_transaction:
id: ocpp_cp
id_tag: "auto"
on_release:
- ocpp.end_transaction:
id: ocpp_cp
id_tag: "auto" # optional; absent → CSMS-initiated stop path
reason: "Local" # optional; defaults to "Local"See example/rippleon.yaml for a complete
production config including HA-mirrored measurands and SmartCharging
hookup.
Connector / Transaction lifecycle. Drives MicroOcpp's connector
state machine from the bound plugged binary sensor and a status text
sensor. on_remote_start / on_remote_stop triggers fire on real
StartTransaction / StopTransaction events (not just request
receipt), so they only run after MO has accepted the request and the
connector is actually entering/leaving a transaction.
SmartCharging. on_charging_profile_change fires whenever the
CSMS-effective current/power limit changes. The component also detects
profile-level disable (SetChargingProfile{limit:0}) at the
request-parse layer and force-zeroes Current.Offered /
Power.Offered until the CSMS lifts the disable — which is what evcc
keys its Enabled() check off when status isn't Charging /
SuspendedEVSE.
Dynamic MeterValuesSampledData. The advertised measurand list is
rebuilt every 5 s from currently-bound and currently-fresh sensors.
HA-mirrored measurands drop out of MeterValues automatically when the
underlying entity goes unavailable, when the ESPHome→HA API link is
down, or (for SoC) when the EV-identity gate fails. Reasserts over
CSMS-side ChangeConfiguration writes.
Heartbeat enforcement. Pinning heartbeat_interval re-applies the
configured value every 5 s — covers the post-BootNotification window
where some CSMSes (SteVe, evcc) reset HeartbeatInterval to their own
default. Same pattern applies to meter_value_sample_interval and
stop_txn_sampled_data.
RFID / LocalAuthList. id(ocpp_cp)->start_transaction(idTag) and
id(ocpp_cp)->end_transaction_with_idtag(idTag) drive the OCPP
Authorize flow from a YAML lambda — wire them to your card-reader
component. MicroOcpp auto-handles SendLocalList / GetLocalListVersion
from the CSMS; with MO_USE_FILEAPI=DISABLE_FS (default) the list lives
in RAM only and resets on every boot.
WebSocket keepalive. Custom WS client with 20 s ping cadence and a 60 s pong watchdog. Reconnects 5 s after any drop.
GPLv3. Inherited from MicroOcpp. If you distribute firmware built against this component, you must offer source.