Skip to content

trahfo/ocpp-rails

Repository files navigation

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

  • Open Charge Alliance for the OCPP specification
  • Built with ❀️ for the EV charging community

πŸ“ž Support

πŸ”— Links


Status: πŸ”§ Alpha - Under Active Development
Version: 0.2.0
OCPP: 1.6 Edition 2
Rails: 8.0+
Ruby: 3.3+

About

Rails engine that provides OCPP 1.6 - 2.2 communication for EV charging stations

Resources

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages