https://github.com/shiv3/gocpp
Generics-first OCPP 1.6 / 2.0.1 / 2.1 library for Go — typed CSMS & charge-point APIs, JSON-schema validation, pluggable storage/auth/observability.
https://github.com/shiv3/gocpp
charge-point csms electric-vehicles ev-charging evse generics go golang ocpp ocpp-2-1 websocket
Last synced: 10 days ago
JSON representation
Generics-first OCPP 1.6 / 2.0.1 / 2.1 library for Go — typed CSMS & charge-point APIs, JSON-schema validation, pluggable storage/auth/observability.
- Host: GitHub
- URL: https://github.com/shiv3/gocpp
- Owner: shiv3
- License: mit
- Created: 2026-06-15T06:36:14.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-06-16T01:17:10.000Z (about 1 month ago)
- Last Synced: 2026-06-16T01:17:14.178Z (about 1 month ago)
- Topics: charge-point, csms, electric-vehicles, ev-charging, evse, generics, go, golang, ocpp, ocpp-2-1, websocket
- Language: Go
- Size: 634 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Security: SECURITY.md
Awesome Lists containing this project
- awesome-ev-charging - shiv3/gocpp - Gocpp is a Go library for building typed OCPP 1.6, 2.0.1, and 2.1 CSMS servers and charge point clients with schema validation and pluggable storage, authentication, and observability (OCPP 1.6, 2.0.1, 2.1 · ⭐ 4). (Tools and Resources / OCPP)
README
# gocpp
A generics-first **OCPP** (Open Charge Point Protocol) implementation in Go,
supporting **OCPP 1.6, 2.0.1, and 2.1** from a single module.
> Status: pre-v1, under active development. Public API may still change before v1.0.
## Why gocpp
A from-scratch alternative to `lorenzodonini/ocpp-go` with:
- **All three versions** (1.6 / 2.0.1 / 2.1) generated from the official OCA JSON
schemas — no hand-written, drift-prone message structs.
- **Generics-first API** — typed `On[Req,Resp]` handlers and `Call[Req,Resp]`, so
request/response types are inferred from a single message value.
- **Context-native concurrency** — every connection owns reader/writer/dispatch
goroutines bound to a `context`; teardown flows from one `cancel(cause)`. Race- and
goroutine-leak-tested (`-race` + `goleak`).
- **Two-layer validation** — JSON-Schema validation against the embedded official
schemas (source of truth), plus generated `validate` struct tags.
- **Pluggable** Storage / Auth / Observability with in-memory / NoOp defaults and
Prometheus + OpenTelemetry adapters.
## Install
```sh
go get github.com/shiv3/gocpp
```
Requires Go 1.25+.
## Quick start
### CSMS (central system)
```go
import v16h "github.com/shiv3/gocpp/v16/handlers"
// Embed Unimplemented and override only the messages you handle; the rest
// return a NotSupported CallError automatically.
type csmsHandler struct{ v16h.UnimplementedCSMSHandler }
func (csmsHandler) OnBootNotification(ctx context.Context, c *csms.Conn, req v16msg.BootNotificationRequest) (v16msg.BootNotificationResponse, error) {
return v16msg.BootNotificationResponse{
Status: v16msg.RegistrationStatusAccepted,
CurrentTime: time.Now(),
Interval: 300,
}, nil
}
func main() {
srv := csms.NewServer(csms.WithSubProtocols("ocpp2.1", "ocpp2.0.1", "ocpp1.6"))
if err := v16h.RegisterCSMS(srv, csmsHandler{}); err != nil {
log.Fatal(err)
}
log.Fatal(srv.ListenAndServe(":8080")) // ws://host:8080/ocpp/{cpId}
}
```
> Need just one message, or dynamic registration? The lower-level
> `csms.On(srv, v16p.BootNotification, handlerFunc)` registers a single handler.
### Charge point (client)
```go
import v16client "github.com/shiv3/gocpp/v16/client"
client := cp.NewClient("CP_1", "ws://localhost:8080/ocpp/CP_1", cp.WithSubProtocols("ocpp1.6"))
if err := client.Connect(ctx); err != nil { log.Fatal(err) }
defer client.Close()
// Typed send methods: one method per CP-originated message.
cpc := v16client.NewCP(client)
resp, err := cpc.BootNotification(ctx, v16msg.BootNotificationRequest{
ChargePointVendor: "Acme", ChargePointModel: "Model-X",
})
```
> The wrapper embeds `*cp.Client`, so `cpc.Connect`, `cpc.Close`, etc. still work. The
> low-level `cp.Call(ctx, client, v16p.BootNotification, req)` remains available if you
> need it.
Runnable examples: [`examples/csms-minimal`](examples/csms-minimal),
[`examples/cp-minimal`](examples/cp-minimal).
## Packages
| Import | Purpose |
|---|---|
| `csms` | CSMS server: `NewServer`, `On`, `Call`, `Get` |
| `cp` | Charge point client: `NewClient`, `Connect`, `On`, `Call`, `Run` |
| `v16`, `v201`, `v21` | Per-version metadata + `RegisterSchemas` |
| `v16/messages`, `…/profiles` | Generated message structs + `ocppj.Message` profile vars |
| `v16/client`, `…/client` | Typed send methods: `NewCP(c).BootNotification(…)`, `NewCSMS(conn).Reset(…)` (+ `…Async`) |
| `v16/handlers`, `…/handlers` | Typed handler interfaces + one-call `RegisterCP` / `RegisterCSMS` |
| `core/ocppj` | OCPP-J framing (Call/CallResult/CallError), errors |
| `core/dispatcher` | Version-agnostic connection lifecycle + pending-call tracking |
| `core/schema` | JSON-Schema validator + registry |
| `core/auth` | `Authenticator`: `None`, `BasicAuth`, `MTLSFromClientCert` |
| `core/storage` (+ `/memory`) | `ConnectionRegistry`, `MessageRouter`, `TransactionStore`, `ConfigStore` |
| `core/observability` (+ `/metrics/{prom,otel}`) | `Metrics` (NoOp/Prometheus/OpenTelemetry), OTel tracer |
## Addons
Optional extensions live under [`addons/`](addons/), each a **separate nested module** so
their heavy dependencies stay out of the core dependency tree:
| Addon | Purpose |
|---|---|
| [`addons/router-redis`](addons/router-redis/) | `storage.MessageRouter` over Redis Pub/Sub (multi-instance CSMS) |
| [`addons/router-nats`](addons/router-nats/) | `storage.MessageRouter` over NATS request/reply |
| [`addons/router-temporal`](addons/router-temporal/) | Durable Temporal-backed `MessageRouter` (experimental) |
| [`addons/statefsm`](addons/statefsm/) | OCPP 1.6 connector state-machine helper |
| [`addons/tenant`](addons/tenant/) | Multi-tenant partitioning of the pluggable stores |
```sh
go get github.com/shiv3/gocpp/addons/router-redis
```
See [addons/README.md](addons/README.md) for details.
## Validation
Layer 1 (wire): enable strict schema validation on the connection so malformed
inbound messages are rejected with a `CallError` before reaching your handler:
```go
reg := schema.NewRegistry()
v201.RegisterSchemas(reg)
srv := csms.NewServer(
csms.WithSubProtocols("ocpp2.0.1"),
csms.WithSchemaRegistry(reg),
csms.WithStrictSchema(true),
)
```
Schema handling is three-state. With a registry set:
- `WithStrictSchema(true)` — reject invalid messages with `FormationViolation`.
- `WithTolerantSchema()` — log a warning and process anyway (for real chargers that send
undefined enums or extra fields).
- `WithStrictSchema(false)` (default) — validation off.
`cp.WithTolerantSchema()` / `cp.WithStrictSchema()` do the same on the charge-point side.
## Production features
```go
srv := csms.NewServer(
csms.WithAuthenticator(auth.BasicAuth(verify)), // Security Profile 1/2 (verify(cpID, password))
csms.WithMetrics(otelmetrics.New(meterProvider)), // OpenTelemetry metrics (or prom.New for Prometheus)
csms.WithTracerProvider(tp), // OpenTelemetry spans
csms.WithConnectionRegistry(myRegistry), // pluggable
csms.WithCPIDExtractor(extractCPID), // dynamic path routing, e.g. /{org}/{cpId}
csms.WithDuplicatePolicy(csms.DuplicatePolicyRejectNew), // or CloseExisting (default)
)
```
Handlers can read connection metadata from the `*csms.Conn`:
`c.RemoteAddr()`, `c.RequestHeader()`, `c.TLS()`, `c.Subprotocol()`. The authenticator
receives the parsed charge point id: `Authenticate(r *http.Request, cpID string)`. To send
an untyped CSMS→CP operation, use `csms.CallRaw(ctx, conn, action, payloadJSON)`.
## Tooling
```sh
go install github.com/shiv3/gocpp/cmd/gocpp-validate@latest
go install github.com/shiv3/gocpp/cmd/gocpp-sim@latest
```
- `gocpp-validate --version 2.0.1 --action BootNotification msg.json` — validate a
message against the official schema.
- `gocpp-sim run -s scenario.yaml` — drive a simulated charge point through a YAML
scenario against a CSMS.
## Testing
```sh
make test # go test ./...
make test-race # go test -race ./...
make codegen # regenerate v16/v201/v21 from schemas/
```
The `internal/conformance` suite ports lorenzodonini/ocpp-go's per-message test cases
(validation tables + direction enforcement) across all 188 messages of 1.6/2.0.1/2.1.
## Documentation
- [docs/usage.md](docs/usage.md) — usage guide
- [docs/architecture.md](docs/architecture.md) — design & concurrency model
## License
MIT