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

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors