Skip to content

shiv3/gocpp

Repository files navigation

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

go get github.com/shiv3/gocpp

Requires Go 1.25+.

Quick start

CSMS (central system)

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)

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/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/, each a separate nested module so their heavy dependencies stay out of the core dependency tree:

Addon Purpose
addons/router-redis storage.MessageRouter over Redis Pub/Sub (multi-instance CSMS)
addons/router-nats storage.MessageRouter over NATS request/reply
addons/router-temporal Durable Temporal-backed MessageRouter (experimental)
addons/statefsm OCPP 1.6 connector state-machine helper
addons/tenant Multi-tenant partitioning of the pluggable stores
go get github.com/shiv3/gocpp/addons/router-redis

See 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:

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

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

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

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

License

MIT

About

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.

Topics

Resources

License

Contributing

Security policy

Stars

4 stars

Watchers

1 watching

Forks

Packages

 
 
 

Contributors

Languages