skip to Main Content

Building a Type-Safe OCPP Client Library for Kotlin & Android

January 16, 20265 minute read

  

Modernizing EV Charging Infrastructure with Kotlin and Coroutines

Introduction

Electric vehicle adoption is accelerating globally, and with it comes the need for robust charging infrastructure. At the heart of EV charging communication lies OCPP (Open Charge Point Protocol) — the industry standard for communication between charging stations and central management systems.

While working on an EV charging project, I discovered a gap: there was no modern, type-safe Kotlin library for OCPP. Most existing solutions were Java-based, callback-heavy, and didn’t leverage Kotlin’s powerful features. So I built one.

In this article, I’ll walk you through the architecture of OCPP Kotlin — an open-source library that brings OCPP to the Kotlin/Android ecosystem with coroutines, Flow, and compile-time type safety.

What is OCPP?

OCPP is a WebSocket-based protocol used by EV chargers to communicate with backend systems (called CSMS — Charging Station Management System). It defines messages for:

  • Authentication — Authorize users via RFID, app, or credit card
  • Transactions — Start/stop charging sessions
  • Monitoring — Report meter values, connector status
  • Remote Control — Remotely start/stop charging, reset stations

The protocol has two major versions in production:

  • OCPP 1.6 — Widely deployed, simpler message set
  • OCPP 2.0.1 — Modern, feature-rich, more complex

Our library supports both.

Note on Hardware: This library operates at the protocol level, making it compatible with any physical connector type (Type 2, CCS, CHAdeMO, etc.). It supports managing multiple connectors/EVSEs via standard OCPP identifiers, leaving specific hardware control to the firmware.

Architecture Overview

OCPP Architecture

The library is built on a modular architecture separating transport, core logic, and version-specific implementations.

  • ocpp-core — Version-agnostic transport, message parsing, request correlation
  • ocpp-1.6 / ocpp-2.0.1 — Type-safe clients with all message definitions
  • ocpp-android — Android lifecycle integration with ViewModel support

Key Design Decisions

1. Coroutines Over Callbacks

Traditional OCPP libraries use callbacks, leading to callback hell:

// ❌ Traditional callback approach
client.sendBootNotification(request, new Callback<BootNotificationResponse>() {
@Override
public void onSuccess(BootNotificationResponse response) {
client.sendHeartbeat(new Callback<HeartbeatResponse>() {
// More nesting...
});
}
@Override
public void onError(Exception e) { }
});

With Kotlin coroutines, this becomes linear and readable:

// ✅ The coroutine approach
val bootResponse = client.bootNotification(station, reason).getOrThrow()
val heartbeatResponse = client.heartbeat().getOrThrow()

2. Type Safety with Sealed Classes

OCPP messages have complex type hierarchies. We use Kotlin’s sealed classes and enums for compile-time safety:

@Serializable
enum class RegistrationStatusEnumType {
@SerialName("Accepted") Accepted,
@SerialName("Pending") Pending,
@SerialName("Rejected") Rejected
}
// Compiler ensures you handle all cases
when (response.status) {
Accepted -> startHeartbeat()
Pending -> waitForAcceptance()
Rejected -> handleRejection()
}

3. Generic API for Version Independence

Real-world apps often need to support multiple OCPP versions. We created a GenericOcppClient interface:

interface GenericOcppClient {
suspend fun bootNotification(model: String, vendor: String): Result<BootNotificationResult>
suspend fun startTransaction(connectorId: Int, idToken: String, meterStart: Int): Result<TransactionResult>
// ... version-agnostic operations
}
// Usage - same code works for both versions!
val client: GenericOcppClient = when (config.version) {
"1.6" -> GenericOcpp16Adapter()
"2.0.1" -> GenericOcpp201Adapter()
}

4. Android Lifecycle Integration

For Android apps, we provide lifecycle-aware ViewModels:

class ChargingViewModel : Ocpp201ViewModel() {

init {
// Connection state automatically observed
connectionState.collect { state ->
when (state) {
is Connected -> updateUI()
is Disconnected -> showReconnecting()
}
}
}

fun startCharging(customerId: String) {
viewModelScope.launch {
authorize(customerId)
.onSuccess { startTransaction(...) }
.onFailure { showError(it) }
}
}
}

Handling the OCPP Message Format

OCPP uses a JSON-RPC-like format over WebSocket:

// Request: [MessageTypeId, MessageId, Action, Payload][2, "abc123", "BootNotification", {"chargingStation": {...}, "reason": "PowerUp"}]// Response: [MessageTypeId, MessageId, Payload]  
[3, "abc123", {"status": "Accepted", "interval": 300}]

The message parser handles this with kotlinx.serialization:

sealed class OcppMessage {
data class Call(val messageId: String, val action: String, val payload: JsonObject) : OcppMessage()
data class CallResult(val messageId: String, val payload: JsonObject) : OcppMessage()
data class CallError(val messageId: String, val errorCode: String, val description: String) : OcppMessage()
}

Request-Response Correlation

One challenge with WebSocket protocols is matching responses to requests. We use a CompletableDeferred map:

private val pendingRequests = ConcurrentHashMap<String, CompletableDeferred<CallResult>>()
suspend fun sendCall(action: String, payload: JsonObject): Result<CallResult> {
val messageId = UUID.randomUUID().toString()
val deferred = CompletableDeferred<CallResult>()
pendingRequests[messageId] = deferred

transport.send(formatCall(messageId, action, payload))

return withTimeoutOrNull(30.seconds) {
deferred.await()
}?.let { Result.success(it) }
?: Result.failure(TimeoutException())
}

When a response arrives, we complete the corresponding deferred:

private fun handleCallResult(result: CallResult) {
pendingRequests.remove(result.messageId)?.complete(result)
}

Auto-Reconnection with Exponential Backoff

Network reliability is crucial for charging stations. We implemented automatic reconnection:

private suspend fun reconnectWithBackoff() {
var delay = 1.seconds
val maxDelay = 60.seconds

while (isActive) {
try {
connect(lastUrl, lastChargePointId)
return // Success!
} catch (e: Exception) {
delay(delay)
delay = minOf(delay * 2, maxDelay) // Exponential backoff
}
}
}

Testing with the Simulator

The library includes a CSMS simulator for testing:

./gradlew :ocpp-simulator:run
# Starts WebSocket server on ws://localhost:8080/ocpp

This lets you test your charging app without a real CSMS backend.

Getting Started

Add JitPack to your settings.gradle.kts:

dependencyResolutionManagement {
repositories {
maven { url = uri("https://jitpack.io") }
}
}

Add dependencies:

dependencies {
implementation("com.github.Jaypatelbond.ocpp-kotlin:ocpp-core:1.1.0")
implementation("com.github.Jaypatelbond.ocpp-kotlin:ocpp-2.0.1:1.1.0")
implementation("com.github.Jaypatelbond.ocpp-kotlin:ocpp-android:1.1.0") // For Android
}

Quick example:

val client = Ocpp201Client()
client.connect("ws://csms.example.com/ocpp", "CP001")
val result = client.bootNotification(
chargingStation = ChargingStationType(model = "FastCharger", vendorName = "MyCompany"),
reason = BootReasonEnumType.PowerUp
)
if (result.status == RegistrationStatusEnumType.Accepted) {
println("Charger registered! Heartbeat every ${result.interval} seconds")
}

What’s Next?

The library is actively maintained with plans for:

  • CSMS Server Module — Build your own charging backend
  • SOAP Transport — For legacy OCPP 1.5 systems
  • More Functional Blocks — ISO 15118, Smart Charging profiles

Conclusion

Building a protocol library taught me the value of:

  • Type safety — Catch errors at compile time, not runtime
  • Coroutines — Clean async code that’s easy to reason about
  • Modularity — Let users pick only what they need

If you’re working on EV charging infrastructure, give OCPP Kotlin a try. Contributions and feedback are welcome!

GitHub: github.com/Jaypatelbond/ocpp-kotlin
Happy Coding! ⚡️


Building a Type-Safe OCPP Client Library for Kotlin & Android was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.

 

Web Developer, Web Design, Web Builder, Project Manager, Business Analyst, .Net Developer

No Comments

This Post Has 0 Comments

Leave a Reply

Back To Top