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.
- π‘ 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
- π¨ 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
- β 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
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.
Add this line to your application's Gemfile:
gem "ocpp-rails"Execute the bundle command:
bundle installRun the installation generator:
rails generate ocpp:rails:installThis 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:migrateConfigure 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
endconfig.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:The identity is the last path segment; frames are the bare OCPP arrays (ws://your-server:3000/ocpp/<charge-point-identifier>[2,"id","Action",{β¦}]) with subprotocolocpp1.6.:action_cable(default) β OCPP-J wrapped in the ActionCable JSON protocol. Stations connect tows://your-server:3000/ocpp/cableand must speak the ActionCable subscribe/messagehandshake (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
endBoth 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.
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# 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# 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)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);
}
});# 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.
- Installation & Setup Guide - Detailed installation, configuration, and first steps
- Configuration Options - Complete configuration reference
- Real-Time Monitoring Guide - Monitor charge points, sessions, and meter values in real-time
- Remote Charging Implementation - Complete guide to remote charging workflow with message diagrams
- ActionCable with SQLite - WebSocket configuration using SQLite (no Redis required)
- API Reference - Models, jobs, and helper methods
- Message Reference - OCPP message examples and payloads
- Testing Guide - Running tests, writing new tests, test coverage
- Troubleshooting - Common issues and solutions
- OCPP 1.6 Specification - Full OCPP 1.6 Edition 2 specification
- Documentation Index - Complete documentation overview
-
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
- One row per
-
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
βββββββββββββββββββ βββββββββββββββββββ
β Your Rails App β β Charge Point β
β (Central System)β β (OCPP 1.6) β
ββββββββββ¬βββββββββ ββββββββββ¬βββββββββ
β β
β 1. RemoteStartTransaction β
ββββββββββββββββββββββββββββββββββββββ>β
β β
β 2. StartTransaction β
β<ββββββββββββββββββββββββββββββββββββββ€
β β
β 3. MeterValues (periodic) β
β<ββββββββββββββββββββββββββββββββββββββ€
β β
β 4. RemoteStopTransaction β
ββββββββββββββββββββββββββββββββββββββ>β
β β
β 5. StopTransaction β
β<ββββββββββββββββββββββββββββββββββββββ€
# 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.rbA 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 undertest/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.
- β 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
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.
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 asRemoteStartTransactionJob/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.
- Match the OCTT spec in the test plan β cite the
TC_xxx_CSMSid 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.mdand the summary in this README. - For major changes, open an issue first to discuss the design.
# 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# 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.rbThis gem is available as open source under the terms of the MIT License.
- Open Charge Alliance for the OCPP specification
- Built with β€οΈ for the EV charging community
- Documentation: docs/
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Status: π§ Alpha - Under Active Development
Version: 0.2.0
OCPP: 1.6 Edition 2
Rails: 8.0+
Ruby: 3.3+