Skip to content

RAR/esphome-ocpp-server

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

49 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

esphome-ocpp-server

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.

Status

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.

Repository layout

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.

Quick start (dev CSMS)

cd server
docker-compose up

SteVe 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.

Using the component

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.

What it does

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.

License

GPLv3. Inherited from MicroOcpp. If you distribute firmware built against this component, you must offer source.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors