Project

ocpp-rails

0.0
The project is in a healthy, maintained state
Use this engine to communicate to your EV Charging stations
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

Runtime

>= 8.0.0
 Project Readme

OCPP Rails

OCPP OCTT CS coverage Status License

A Ruby on Rails engine for building OCPP 1.6 Central System (CSMS) backends for Electric Vehicle (EV) charging networks. This is a backend-only gem — you build your own UI while OCPP Rails handles the WebSocket transport, data models, and the OCPP message layer.

⚠️ Early alpha — read the compliance status before you rely on this. The Core charging-session flow (boot, authorize, start/stop transaction, meter values, status), remote start/stop, and connector unlock are implemented and covered by real handler/job-driven tests. Most other Central-System→Charge-Point operations and every optional feature profile (Reservation, Smart Charging, Firmware, Diagnostics, Local Auth List, Security certificates) are not implemented yet. See OCPP 1.6 Compliance Status for the exact, per-test-case picture — and please consider contributing a slice.

✨ Features

OCPP Protocol Layer (What This Gem Provides)

  • 📡 WebSocket Communication - ActionCable channel handles bidirectional OCPP messages
  • 🔌 Protocol Handlers - BootNotification, Authorize, Heartbeat, StartTransaction, StopTransaction, MeterValues, StatusNotification
  • 🗄️ Data Models - ChargePoint, ConnectorStatus, ChargingSession, MeterValue, Message (audit log)
  • 🪝 Lifecycle Hooks - authorization, state-change, and session-start/stop hooks (sync or async via ActiveJob) — react without polling
  • 🚀 Remote Control Jobs - RemoteStartTransaction, RemoteStopTransaction, UnlockConnector, Reset, ClearCache, GetConfiguration, ChangeConfiguration, ChangeAvailability
  • 📊 Real-time Broadcasts - ActionCable broadcasts for status, sessions, and meter values
  • 💾 SQLite Compatible - Works with async adapter, no Redis required for development

What You Build (Your Application)

  • 🎨 User Interface - Build your own dashboard, charts, and controls
  • 🔐 Authentication - Implement your own user authentication
  • 📱 API Endpoints - Create REST/GraphQL APIs as needed
  • 💼 Business Logic - Billing, reservations, user management, etc.
  • 🎯 Custom Authorization - Override handlers for RFID validation logic

OCPP Compliance

  • Core session flow — inbound BootNotification, Authorize, Heartbeat, Start/StopTransaction, MeterValues, StatusNotification
  • Remote Control — RemoteStartTransaction, RemoteStopTransaction, UnlockConnector (delivery + end-to-end flow, tested)
  • Message Audit — every inbound/outbound frame logged for debugging and compliance
  • Multi-connector — one active session per connector, enforced at the DB level; per-connector status tracked independently of whole-station status
  • 🚧 Everything else — see the honest, per-test-case OCPP 1.6 Compliance Status below

📋 OCPP 1.6 Compliance Status

Compliance here is measured against the Open Charge Alliance OCPP Compliance Testing Tool (OCTT) test case document (2025-02), Section 3 — the cases that apply when the System Under Test is the Central System. There are 76 such cases. This is the role ocpp-rails fills, so it is the right yardstick.

Where we are today:

Cases What it means
Implemented + tested 32 Works and guarded by a real handler/job-driven test
🟡 Implemented — needs test 0 Behavior works, but only simulation-style tests exist; needs a real regression test
🔴 Not implemented 42 The message/operation does not exist in the engine yet
Out of scope 2 TLS handshake (TC_086/087) — belongs in your infra, not app code

So 32 of 76 OCTT Central-System cases (42%) are backed by working code, and every one now has real automated (handler/job-driven) coverage — the 🟡 bucket is empty. The entire Core outbound command set is done; the rest — Local Auth List, Firmware, Diagnostics, Reservation, Remote Trigger, Smart Charging, DataTransfer and the Security profiles 2/3 — is not built yet. Treat this gem as a solid, well-tested Core-profile foundation to build on, not a certified CSMS.

By feature area (each links to the detailed Given/When/Then specs):

Area Status Notes
Boot / Charging Sessions / Cache ✅ implemented + tested Core flow and ClearCache tested end-to-end
Remote Start / Stop ✅ implemented + tested delivery and end-to-end session flow tested
Reset / Unlock / Configuration ✅ implemented + tested Reset, UnlockConnector, Get/ChangeConfiguration all tested (+ ChangeAvailability)
Authorize non-happy paths ✅ tested Invalid / Expired / Blocked on Authorize.req tested
Offline / power-loss ✅ tested replay + power-loss recovery sequences tested
Local Authorization List 🔴 not implemented no SendLocalList / GetLocalListVersion
Firmware Management 🔴 not implemented no UpdateFirmware / FirmwareStatusNotification
Diagnostics 🔴 not implemented no GetDiagnostics / DiagnosticsStatusNotification
Reservation 🔴 not implemented no ReserveNow / CancelReservation, no model
Remote Trigger 🔴 not implemented no TriggerMessage
Smart Charging 🔴 not implemented no SetChargingProfile / ClearChargingProfile / GetCompositeSchedule
DataTransfer 🔴 not implemented inbound DataTransfer currently gets a NotSupported CALLERROR
Security (profiles 1–3) 🟡 Basic auth only HTTP Basic Auth works + tested; certificates/secure firmware/TLS not

👉 Full per-test-case breakdown with Given/When/Then specs: docs/octt-test-plan.md. It doubles as a ready-made contribution backlog — every 🔴 and 🟡 is a self-contained PR.

🚀 Quick Start

Installation

Add this line to your application's Gemfile:

gem "ocpp-rails"

Execute the bundle command:

bundle install

Setup

Run the installation generator:

rails generate ocpp:rails:install

This will:

  • ✅ Create database migrations for charge points, sessions, and meter values
  • ✅ Mount the engine at /ocpp (ActionCable WebSocket endpoint)
  • ✅ Generate an initializer at config/initializers/ocpp_rails.rb
  • ✅ Configure ActionCable for SQLite compatibility
  • ✅ Display setup instructions

Run the migrations:

rails db:migrate

Configuration

Configure OCPP settings in config/initializers/ocpp_rails.rb:

Ocpp::Rails.setup do |config|
  config.ocpp_version = "1.6"
  config.supported_versions = ["1.6"]
  config.heartbeat_interval = 300  # 5 minutes
  config.connection_timeout = 30   # 30 seconds
end

Transports

config.transport selects how charge points reach the engine:

  • :raw — native bare OCPP-J over a plain WebSocket, which is what real/commercial charge points speak. Stations connect to:
    ws://your-server:3000/ocpp/<charge-point-identifier>
    
    The identity is the last path segment; frames are the bare OCPP arrays ([2,"id","Action",{…}]) with subprotocol ocpp1.6.
  • :action_cable (default) — OCPP-J wrapped in the ActionCable JSON protocol. Stations connect to ws://your-server:3000/ocpp/cable and must speak the ActionCable subscribe/message handshake (a generic OCPP station cannot; this is aimed at ActionCable-aware clients/simulators). Kept as the default for backwards compatibility.
  • :both — mount both endpoints side by side (useful while migrating).
Ocpp::Rails.setup do |config|
  config.transport = :raw   # accept real OCPP-J wallboxes directly
end

Both transports authenticate identically: HTTP Basic Auth on the WebSocket upgrade (OCPP-J Security Profile 1, enabled by default; the Basic username must equal the station identity). Provision a per-station credential first:

charge_point.update!(auth_password: SecureRandom.base58(32))

For internet-facing deployments use TLS (wss://, Security Profile 2), terminated by a reverse proxy in front of the app.

For detailed setup instructions, see the Getting Started Guide and the Security Guide.

💡 Usage Examples

Monitor Charge Point Status

ChargePoint#status is the whole-station status (from connector-0 StatusNotifications: Available/Unavailable/Faulted) — it says nothing about any individual connector once a station has more than one.

# Query charge points
connected_cps = Ocpp::Rails::ChargePoint.connected
available_cps = Ocpp::Rails::ChargePoint.available
charging_cps = Ocpp::Rails::ChargePoint.charging  # any active session, any connector

# Check specific charge point
cp = Ocpp::Rails::ChargePoint.find_by(identifier: "CP001")
cp.connected?       # => true/false
cp.status           # => whole-station status: "Available", "Unavailable", "Faulted"
cp.last_heartbeat_at

# Per-connector status (independent of any other connector on the same station)
cp.connector_status(1)      # => "Available", "Charging", "SuspendedEV", ...
cp.connector_charging?(1)   # => true/false, derived from active sessions

Monitor Active Sessions

# Get active sessions
active_sessions = Ocpp::Rails::ChargingSession.active

# Get session details
session = cp.current_session
session.connector_id
session.energy_consumed  # kWh
session.duration_seconds

Monitor Meter Values

# Get latest readings
latest_energy = cp.meter_values.energy.recent.first
latest_power = cp.meter_values.power.recent.first

# Get readings for a session
session.meter_values.order(:timestamp)

Real-Time Updates via ActionCable

Subscribe to real-time broadcasts in your UI:

# app/channels/meter_values_channel.rb
class MeterValuesChannel < ApplicationCable::Channel
  def subscribed
    charge_point = Ocpp::Rails::ChargePoint.find(params[:charge_point_id])
    stream_from "charge_point_#{charge_point.id}_meter_values"
  end
end
// Subscribe in JavaScript
subscribeToMeterValues(chargePointId, {
  onPower: (data) => {
    updatePowerGauge(data.value);
  },
  onEnergy: (data) => {
    updateEnergyCounter(data.value);
  }
});

Remote Control

# Start a charging session
charge_point = Ocpp::Rails::ChargePoint.find_by(identifier: "CP001")

Ocpp::Rails::RemoteStartTransactionJob.perform_later(
  charge_point.id,
  1,              # connector_id
  "RFID12345"     # id_tag
)

# Stop a charging session
session = charge_point.current_session
Ocpp::Rails::RemoteStopTransactionJob.perform_later(
  charge_point.id,
  session.transaction_id
)

For complete implementation examples, see the Remote Charging Guide.

📚 Documentation

Getting Started

Implementation Guides

Development

Reference

🏗️ Architecture

Models

  • Ocpp::Rails::ChargePoint - Represents a physical charging station

    • Tracks connection status, firmware version, vendor info
    • Has many charging sessions, connector statuses, and meter values
    • Provides status scopes (connected, available, charging) and per-connector reads (connector_status, connector_charging?)
  • Ocpp::Rails::ConnectorStatus - Last reported status for one connector

    • One row per (charge_point, connector_id), written by StatusNotification
    • Independent of whole-station status and of any other connector's activity
  • Ocpp::Rails::ChargingSession - Represents a charging transaction

    • Manages session lifecycle (active/completed)
    • Calculates energy consumption and duration automatically
    • Links to meter values for detailed tracking
  • Ocpp::Rails::MeterValue - Stores individual meter readings

    • Supports 22+ OCPP measurands (Energy, Power, Current, Voltage, SoC, Temperature, etc.)
    • Can be transaction-specific or standalone
    • Includes timestamp, phase, context, and location
  • Ocpp::Rails::Message - Logs all OCPP communications

    • Stores direction (inbound/outbound)
    • Includes full message payload as JSONB
    • Enables debugging and compliance auditing

Communication Flow

┌─────────────────┐                    ┌─────────────────┐
│  Your Rails App │                    │  Charge Point   │
│  (Central System)│                   │     (OCPP 1.6)  │
└────────┬────────┘                    └────────┬────────┘
         │                                      │
         │  1. RemoteStartTransaction           │
         ├─────────────────────────────────────>│
         │                                      │
         │  2. StartTransaction                 │
         │<─────────────────────────────────────┤
         │                                      │
         │  3. MeterValues (periodic)           │
         │<─────────────────────────────────────┤
         │                                      │
         │  4. RemoteStopTransaction            │
         ├─────────────────────────────────────>│
         │                                      │
         │  5. StopTransaction                  │
         │<─────────────────────────────────────┤

🧪 Testing

# Run all tests
rails test

# Run the handler/job-driven unit tests (the ones that exercise real code)
rails test test/ocpp/

# Run a specific test file
rails test test/ocpp/outbound_delivery_test.rb

A note on what the tests prove. The suite has two kinds of tests:

  • Handler/job-driven tests push real frames through MessageHandler/Actions::*Handler/jobs and assert on actual behavior — e.g. test/ocpp/message_handler_test.rb, outbound_delivery_test.rb, station_authentication_test.rb, start_transaction_authorization_test.rb, authorize_handler_test.rb, boot_notification_handler_test.rb, status_notification_handler_test.rb, unlock_connector_job_test.rb, and several under test/ocpp/integration/ (remote_start_flow_test.rb, remote_stop_flow_test.rb, offline_transaction_test.rb, power_failure_recovery_test.rb). These are what the compliance status above counts.
  • Simulation-style tests — the older files under test/ocpp/integration/ (e.g. *_transaction_test.rb, remote_charging_session_workflow_test.rb, authorize_test.rb, boot_notification_test.rb) build request/response hashes and model rows by hand without invoking production code. They document expected message shapes but do not prove wire-protocol compliance, so they are not counted toward OCTT coverage.

When you implement a 🔴/🟡 case from the test plan, please add a handler/job-driven test for it. See the Testing Guide for details.

🗺️ Roadmap

Implemented today

  • ✅ Core inbound session flow (Boot, Authorize, Heartbeat, Start/StopTransaction, MeterValues, StatusNotification)
  • ✅ Remote start/stop transactions (delivery + end-to-end flow) and UnlockConnector — releasable even during an active session
  • ✅ Device management: Reset (Hard/Soft), ClearCache, ChangeAvailability, and Get/ChangeConfiguration
  • ✅ Real-time meter value / status / session broadcasts
  • ✅ Session management, energy/duration tracking, meter-anomaly + timestamp-provenance checks
  • ✅ OCPP-J Security Profile 1 (HTTP Basic Auth) + per-station rate limiting
  • ✅ Complete inbound/outbound message logging
  • ✅ Handler/job-driven OCTT regression suite for all 32 implemented Core-profile cases

Not implemented yet (contributions very welcome — see the test plan)

  • 🔴 Local Authorization List (SendLocalList, GetLocalListVersion)
  • 🔴 Firmware updates + FirmwareStatusNotification
  • 🔴 Diagnostics upload + DiagnosticsStatusNotification
  • 🔴 Reservation system (ReserveNow, CancelReservation)
  • 🔴 Remote Trigger (TriggerMessage)
  • 🔴 Smart charging profile management (Set/Clear/GetCompositeSchedule)
  • 🔴 Inbound DataTransfer handling
  • 🔴 Security profiles 2/3 (certificate management, secure firmware, security events)
  • 📝 OCPP 2.0.1 support

🤝 Contributing

This project needs you. It's an honest, well-tested Core-profile foundation with a long list of OCPP features still to build — and that list is already written up as a ready-to-pick-up backlog.

Where to start

docs/octt-test-plan.md maps all 76 OCTT Central-System test cases to their status. Every 🔴 (not implemented) and 🟡 (needs a real test) entry comes with a Given/When/Then spec and a suggested test file — so each one is a self-contained, well-scoped PR. Good first issues:

  • 🟡 Add a real test for something that already works — pick a 🟡 case (e.g. the Authorize non-happy paths, or the boot flow) and write a handler-driven test against the spec. No new production code needed.
  • 🔴 Implement one operation — the outbound Core command set is complete; the next self-contained targets are Remote Trigger (TriggerMessage) and the Local Auth List commands (GetLocalListVersion, SendLocalList), following the same job pattern as RemoteStartTransactionJob / ResetJob.
  • 🔴 Build out a feature profile — Reservation, Smart Charging, Firmware, etc. are larger efforts; open an issue first so we can sketch the model/API together.

Ground rules

  • Match the OCTT spec in the test plan — cite the TC_xxx_CSMS id in your PR.
  • Add a handler/job-driven test (not a simulation-style hash test) for any behavior you implement, and update the case's status in docs/octt-test-plan.md and the summary in this README.
  • For major changes, open an issue first to discuss the design.

Development Setup

# Clone the repository
git clone https://github.com/trahfo/ocpp-rails.git
cd ocpp-rails

# Install dependencies
bundle install

# Setup test database
cd test/dummy
rails db:migrate RAILS_ENV=test
cd ../..

# Run tests
rails test

Running the Test Suite

# Run all tests with coverage
rails test

# Run with verbose output
rails test -v

# Run specific test file
rails test test/ocpp/integration/remote_charging_session_workflow_test.rb

📄 License

This gem is available as open source under the terms of the MIT License.

🙏 Acknowledgments

📞 Support

🔗 Links


Status: 🔧 Alpha - Under Active Development
Version: 0.2.0
OCPP: 1.6 Edition 2
Rails: 8.0+
Ruby: 3.3+