A type-safe Go implementation of the Open Charge Point Protocol (OCPP) 1.6 message set, with validated request/response structures for EV charge points (EVSE) and Central Systems (CSMS/backends).
This library implements OCPP (Open Charge Point Protocol) 1.6 message types with strict validation, following Go best practices and the official OCPP 1.6 specification. It is designed as a foundation for building OCPP-compliant charging station management systems and charge point implementations.
Status: Stable - v1.0.0 (28/28 OCPP 1.6 JSON messages implemented)
- API follows SemVer; see [CHANGELOG](CHANGELOG.md) for release notes.
- Public API is frozen for all v1.x releases; breaking changes will bump
MAJOR.
Search terms: OCPP 1.6, Open Charge Point Protocol, EVSE, CSMS, charge station backend, Authorize.req, BootNotification, MeterValues, RemoteStart, Charge Point, Central System.
- Type Safety - Constructor pattern with validation
(
New*()for types,Req()/Conf()for messages) - OCPP 1.6 Compliance - Strict adherence to protocol specification
- OCPP Naming - Uses
Req()/Conf()to match OCPP terminology (Authorize.req, Authorize.conf) - Immutable Types - Thread-safe by design with value receivers
- Comprehensive Testing - Unit tests and example tests with high coverage
- Zero Panics - All errors returned, never panicked
- Well Documented - Full godoc coverage and examples
The library covers the full OCPP 1.6 message surface, including:
- Authorize.req / .conf, BootNotification.req / .conf, Heartbeat.req / .conf
- StartTransaction.req / .conf, StopTransaction.req / .conf, StatusNotification
- MeterValues, ClearChargingProfile, SetChargingProfile, TriggerMessage
- RemoteStartTransaction, RemoteStopTransaction, ChangeAvailability, ChangeConfiguration, GetConfiguration, DataTransfer
- ReserveNow, CancelReservation, UnlockConnector, Reset, UpdateFirmware, GetDiagnostics, DiagnosticsStatusNotification, FirmwareStatusNotification
- SendLocalList, GetLocalListVersion, GetCompositeSchedule
go get github.com/aasanchez/ocpp16messages
Requirements: Go 1.25.8 or later (CI and go.mod aligned)
.
├── types/ # Core OCPP data types (shared across messages)
│ ├── cistring.go # CiString20/25/50/255/500 types
│ ├── datetime.go # RFC3339 DateTime with UTC normalization
│ ├── integer.go # Validated uint16 Integer type
│ ├── errors.go # Shared error constants and sentinels
│ ├── authorizationstatus.go # AuthorizationStatus enum
│ ├── idtoken.go # IdToken type
│ ├── idtaginfo.go # IdTagInfo type
│ ├── chargingprofilepurposetype.go # ChargingProfilePurposeType enum
│ ├── chargingrateunit.go # ChargingRateUnit enum
│ ├── chargingschedule.go # ChargingSchedule type
│ ├── chargingscheduleperiod.go # ChargingSchedulePeriod type
│ ├── metervalue.go # MeterValue and MeterValueInput types
│ ├── sampledvalue.go # SampledValue and SampledValueInput types
│ ├── measurand.go # Measurand enum
│ ├── readingcontext.go # ReadingContext enum
│ ├── valueformat.go # ValueFormat enum
│ ├── phase.go # Phase enum
│ ├── location.go # Location enum
│ ├── unitofmeasure.go # UnitOfMeasure enum
│ ├── doc.go # Package documentation
│ └── tests/ # Public API tests (black-box)
├── authorize/ # Authorize message
├── bootNotification/ # BootNotification message
├── cancelReservation/ # CancelReservation message
├── changeAvailability/ # ChangeAvailability message
├── changeConfiguration/ # ChangeConfiguration message
├── clearCache/ # ClearCache message
├── clearChargingProfile/ # ClearChargingProfile message
├── dataTransfer/ # DataTransfer message
├── diagnosticsStatusNotification/ # DiagnosticsStatusNotification message
├── firmwareStatusNotification/ # FirmwareStatusNotification message
├── getCompositeSchedule/ # GetCompositeSchedule message
├── getConfiguration/ # GetConfiguration message
├── getDiagnostics/ # GetDiagnostics message
├── getLocalListVersion/ # GetLocalListVersion message
├── heartbeat/ # Heartbeat message
├── meterValues/ # MeterValues message
├── remoteStartTransaction/ # RemoteStartTransaction message
├── remoteStopTransaction/ # RemoteStopTransaction message
├── reserveNow/ # ReserveNow message
├── reset/ # Reset message
├── sendLocalList/ # SendLocalList message
├── setChargingProfile/ # SetChargingProfile message
├── startTransaction/ # StartTransaction message
├── statusNotification/ # StatusNotification message
├── stopTransaction/ # StopTransaction message
├── triggerMessage/ # TriggerMessage message
├── unlockConnector/ # UnlockConnector message
├── updateFirmware/ # UpdateFirmware message
└── SECURITY.md # Security policy and vulnerability reporting
- Semantic Versioning: API surface follows SemVer starting with v1.0.0.
- Compatibility contract: see COMPATIBILITY.md for exact SemVer guarantees, deprecation policy, and what counts as breaking.
- Supported Go versions: >= 1.25 (aligned with go.mod and CI).
- Changelog: see CHANGELOG for releases and upgrade notes.
The library provides validated OCPP 1.6 data types:
import "github.com/aasanchez/ocpp16messages/types"
// CiString types (case-insensitive, ASCII printable, length-validated)
idTag, err := types.NewCiString20Type("RFID-ABC123")
if err != nil {
// Handle validation error (length > 20 or non-ASCII chars)
}
// DateTime (RFC3339, must be UTC)
timestamp, err := types.NewDateTime("2025-01-02T15:04:05Z")
if err != nil {
// Handle parsing error
}
// Integer (validated uint16)
retryCount, err := types.NewInteger(3)
if err != nil {
// Handle conversion/range error
}
Messages use OCPP terminology with Req() for requests and Conf() for responses:
import "github.com/aasanchez/ocpp16messages/authorize"
// Create an Authorize.req message using the ReqInput struct
// Validation happens automatically in the constructor
req, err := authorize.Req(authorize.ReqInput{
IdTag: "RFID-ABC123",
})
if err != nil {
// Handle validation error (empty, too long, or invalid characters)
}
// Access the validated IdTag
fmt.Println(req.IdTag.String()) // "RFID-ABC123"
import "github.com/aasanchez/ocpp16messages/clearChargingProfile"
// ClearChargingProfile.req with optional fields
id := 123
req, err := clearChargingProfile.Req(clearChargingProfile.ReqInput{
Id: &id,
ConnectorId: nil,
ChargingProfilePurpose: nil,
StackLevel: nil,
})
The ReqMessage type returned by Req() contains validated, typed fields.
Core value types in types/ are immutable and thread-safe. Message structs
are safe to share between goroutines as long as they are treated as
read-only (they have exported fields, so consumers can mutate them).
This library aims to provide stable error identities and flexible error messages.
- Stable: use
errors.Is(err, types.ErrEmptyValue)anderrors.Is(err, types.ErrInvalidValue)to detect validation failures. - Stable: constructors may return an aggregated error using
errors.Join; callers should useerrors.Israther than string matching. - Not stable: exact error strings and formatting (including joined error order) may change between releases.
All validation happens at construction time. A non-nil error always means the value/message was not constructed.
Detect the common failure modes via errors.Is:
import (
"errors"
"fmt"
"github.com/aasanchez/ocpp16messages/authorize"
"github.com/aasanchez/ocpp16messages/types"
)
req, err := authorize.Req(authorize.ReqInput{
IdTag: "",
})
if err != nil {
switch {
case errors.Is(err, types.ErrEmptyValue):
fmt.Println("missing required field")
case errors.Is(err, types.ErrInvalidValue):
fmt.Println("invalid value (length, charset, range, enum, etc.)")
default:
fmt.Println("unexpected error:", err)
}
}
_ = req
Notes:
- Many constructors aggregate multiple field errors with
errors.Join(...).errors.Isstill works against joined errors. - Avoid depending on exact error strings; they may change as long as
errors.Isbehavior remains stable.
- Go 1.25+
- golangci-lint
- staticcheck
- gci, gofumpt, golines (formatters)
If you work in an environment with private forks or private module dependencies, configure the Go toolchain so it does not use the public checksum database for those module paths:
export GOPRIVATE=github.com/<your-org>/*
export GONOSUMDB=$GOPRIVATE
export GONOPROXY=$GOPRIVATE
- Why does
NewDateTimereject+02:00timestamps?- OCPP 1.6 requires UTC. This library enforces UTC-only dateTimes, so provide
values like
2025-01-02T15:04:05Z.
- OCPP 1.6 requires UTC. This library enforces UTC-only dateTimes, so provide
values like
- Nil vs empty slices: what's the difference?
- Nil means "field omitted"; empty means "field present but empty". Message constructors preserve this distinction.
- Are messages immutable?
- Core types in
types/are immutable. Message structs have exported fields, so they are concurrency-safe only when treated as read-only. SeeROADMAP.mdfor the v2 plan to make messages structurally immutable.
- Core types in
This repository uses a layered test strategy:
- Unit tests (default): fast, deterministic, run on every CI push/PR via
go test ./...andmake test. - Example tests (default): executable docs that demonstrate intended usage
(
go test -run '^Example' ./...andmake test-example). - Fuzz tests (opt-in): invariants and validation hardening under
./tests_fuzzwith build tagfuzz(make test-fuzz). - Race tests (opt-in): immutability/aliasing and concurrency guarantees
under
./tests_racewith build tagrace(make test-race). - Benchmarks (opt-in): performance regression guards under
./tests_benchmarkwith build tagbench(make test-bench).
Opt-in suites are not part of default go test ./... by design; they are
heavier and run on a weekly schedule in CI (and on-demand locally).
Benchmarks are opt-in and intended to catch regressions in high-value constructors and worst-case payloads.
Local workflow:
make test-bench
go install golang.org/x/perf/cmd/benchstat@latest
benchstat old.txt new.txt
On releases (tag pushes), CI generates a benchstat comparison against the
previous tag and attaches it to the GitHub release assets.
See ADDING_MESSAGE.md for a minimal, copy/paste-friendly template that
matches the project's constructor + validation style and test organization.
# Install dependencies
go mod tidy
# Run tests
make test # Unit tests with coverage
make test-coverage # Generate HTML coverage report
make test-example # Run example tests (documentation tests)
make test-all # Run all test types
make test-race # Run race detector with -race (opt-in)
make test-fuzz # Run fuzzers in ./tests_fuzz (short budget, opt-in)
make test-bench # Run benchmarks in ./tests_benchmark (opt-in)
# Code quality
make lint # Run all linters (golangci-lint, go vet, staticcheck)
make format # Format code (gci, gofumpt, golines, gofmt)
# Documentation
make pkgsite # Start local documentation server at http://localhost:8080
This library is designed for safe concurrent use when values are treated as immutable. To keep this guarantee strong over time, any new public API or constructor changes must follow these rules.
- Never store pointers to caller-owned variables in returned values.
- Example: if an input field is
*string, copy*inputinto a new local variable and store a pointer to the copy.
- Example: if an input field is
- Never expose internal slices directly.
- If a getter returns a slice, return a copy of the slice header/data.
- If a constructor stores a slice, allocate a new slice and copy elements.
- Never expose internal pointers directly.
- If a getter returns a
*T, return a pointer to a copy ofT.
- If a getter returns a
These patterns prevent accidental mutation of internal state and eliminate real race hazards (e.g., aliasing an input pointer, then the caller mutating that variable concurrently with reads of the returned message/type).
Race tests live in ./tests_race behind the race build tag (//go:build race).
They are not part of default go test ./....
- Add/update race tests for:
- Every new
Req()/Conf()constructor. - Every new exported
New*constructor. - Every new exported getter that returns a pointer or a slice.
- Every new
- Race tests should be high-signal:
- Use shared inputs across goroutines to catch unintended mutation.
- Include "immutability" tests that mutate a returned pointer/slice and assert the original value is unchanged.
- Do not call
t.Fatal*from goroutines. Use the shared concurrency helper inrace/helpers_test.go(runConcurrent).
Run locally via:
make test-race
- TODO(v2): Redesign
ReqMessage/ConfMessagestructs to be truly immutable (unexported fields + getters and/or deep-copy semantics). Today, message structs have exported fields, so they are concurrency-safe only when treated as read-only.
Fuzzers live in ./tests_fuzz and are guarded by the fuzz build tag, so they
do not run as part of go test ./....
Run fuzzers via the project helper:
make test-fuzz
Tuning knobs:
FUZZTIME(default5s): per-fuzzer time budget.FUZZPROCS(default4): caps fuzz worker parallelism viaGOMAXPROCS.
To run a single fuzzer directly:
go test -tags=fuzz -run=^$ -fuzz=^FuzzAuthorizeReq$ -fuzztime=10s ./tests_fuzz
The fuzz suite is intentionally "high-scrutiny" (high-signal):
- All message constructors: every OCPP operation package has fuzzers for
both
Req()andConf()(including empty constructors that must always succeed). - Core validation types: fuzzers exist for constructors like
NewDateTime,NewInteger, allNewCiString*Typevariants,NewChargingScheduleandNewChargingSchedulePeriod,NewIdTagInfo,NewSampledValue, andNewMeterValue(plus their message-scoped equivalents). - Enum correctness: strict membership fuzzers validate that every
IsValid()enum acrosstypes/and all*/typessubpackages returns true if and only if the input matches one of the OCPP-spec values. - Error semantics: when construction fails, fuzzers require the returned
error to wrap the shared sentinel validation errors
(
types.ErrEmptyValue/types.ErrInvalidValue). - Success invariants: when construction succeeds, fuzzers assert strong
postconditions (range checks, nil vs empty slice preservation, UTC-only
DateTimevalues, and round-tripping of string/int fields).
For local development, .vscode/settings.json configures gopls to include
-tags=fuzz,race,bench so the ./tests_fuzz, ./tests_race, and
./tests_benchmark test packages are indexed in the editor.
- Weekly workflow runs
make test-all,make test-race,make test-fuzz, andmake test-benchto guard the opt-in suites.
Reports are generated in the reports/ directory:
reports/coverage.out- Coverage datareports/golangci-lint.txt- Lint results
| OCPP Type | Go Type | Validation |
|---|---|---|
| CiString20Type | types.CiString20Type |
Length <= 20, ASCII printable (32-126) |
| CiString25Type | types.CiString25Type |
Length <= 25, ASCII printable (32-126) |
| CiString50Type | types.CiString50Type |
Length <= 50, ASCII printable (32-126) |
| CiString255Type | types.CiString255Type |
Length <= 255, ASCII printable (32-126) |
| CiString500Type | types.CiString500Type |
Length <= 500, ASCII printable (32-126) |
| dateTime | types.DateTime |
RFC3339, UTC only |
| integer | types.Integer |
uint16 (0-65535) |
| OCPP Type | Go Type | Description |
|---|---|---|
| IdToken | types.IdToken |
RFID tag identifier (CiString20) |
| IdTagInfo | types.IdTagInfo |
Authorization info with status |
| AuthorizationStatus | types.AuthorizationStatus |
Accepted, Blocked, Expired, etc |
| OCPP Type | Go Type | Description |
|---|---|---|
| ChargingProfilePurposeType | types.ChargingProfilePurposeType |
TxDefaultProfile, TxProfile |
| ChargingRateUnit | types.ChargingRateUnit |
W or A |
| ChargingSchedule | types.ChargingSchedule |
Schedule with periods |
| ChargingSchedulePeriod | types.ChargingSchedulePeriod |
Start/limit/phases |
| OCPP Type | Go Type | Description |
|---|---|---|
| MeterValue | types.MeterValue |
Timestamp + SampledValue array |
| SampledValue | types.SampledValue |
Value + optional context/format/etc |
| Measurand | types.Measurand |
Energy, Power, Current, Voltage, etc |
| ReadingContext | types.ReadingContext |
Sample.Clock, Sample.Periodic, etc |
| ValueFormat | types.ValueFormat |
Raw or SignedData |
| Phase | types.Phase |
L1, L2, L3, N, L1-N, L2-N, L3-N |
| Location | types.Location |
Body, Cable, EV, Inlet, Outlet |
| UnitOfMeasure | types.UnitOfMeasure |
Wh, kWh, varh, kvarh, W, kW, VA, etc |
| Package | Type | Description |
|---|---|---|
bootNotification/types |
RegistrationStatus |
Accepted, Pending, Rejected |
cancelReservation/types |
CancelReservationStatus |
Accepted, Rejected |
changeAvailability/types |
AvailabilityType |
Inoperative, Operative |
changeAvailability/types |
AvailabilityStatus |
Accepted, Rejected, Scheduled |
changeConfiguration/types |
ConfigurationStatus |
Accepted, Rejected, etc |
clearCache/types |
ClearCacheStatus |
Accepted, Rejected |
clearChargingProfile/types |
ClearChargingProfileStatus |
Accepted, Unknown |
dataTransfer/types |
DataTransferStatus |
Accepted, Rejected, etc |
diagnosticsStatusNotification |
DiagnosticsStatus |
Idle, Uploaded, UploadFailed, etc |
firmwareStatusNotification |
FirmwareStatus |
Downloaded, Installing, etc |
getCompositeSchedule/types |
GetCompositeScheduleStatus |
Accepted, Rejected |
getConfiguration/types |
KeyValue |
Configuration key-value pair |
getLocalListVersion/types |
ListVersionNumber |
Local list version number |
remoteStartTransaction/types |
RemoteStartTransactionStatus |
Accepted, Rejected |
remoteStopTransaction/types |
RemoteStopTransactionStatus |
Accepted, Rejected |
reserveNow/types |
ReservationStatus |
Accepted, Faulted, Occupied, etc |
reset/types |
ResetType |
Hard, Soft |
reset/types |
ResetStatus |
Accepted, Rejected |
sendLocalList/types |
UpdateType |
Differential, Full |
sendLocalList/types |
UpdateStatus |
Accepted, Failed, etc |
sendLocalList/types |
AuthorizationData |
IdTag + IdTagInfo |
setChargingProfile/types |
ChargingProfile |
Complete charging profile |
setChargingProfile/types |
ChargingProfileKindType |
Absolute, Recurring, Relative |
setChargingProfile/types |
ChargingProfileStatus |
Accepted, Rejected, etc |
setChargingProfile/types |
RecurrencyKindType |
Daily, Weekly |
statusNotification/types |
ChargePointErrorCode |
ConnectorLockFailure, etc |
statusNotification/types |
ChargePointStatus |
Available, Charging, Faulted, etc |
stopTransaction/types |
StopReason |
EmergencyStop, EVDisconnected, etc |
triggerMessage/types |
MessageTrigger |
BootNotification, Heartbeat, etc |
triggerMessage/types |
TriggerMessageStatus |
Accepted, Rejected, NotImplemented |
unlockConnector/types |
UnlockStatus |
Unlocked, UnlockFailed, etc |
| Message | Request | Confirmation | Package |
|---|---|---|---|
| Authorize | Done | Done | authorize |
| BootNotification | Done | Done | bootNotification |
| CancelReservation | Done | Done | cancelReservation |
| ChangeAvailability | Done | Done | changeAvailability |
| ChangeConfiguration | Done | Done | changeConfiguration |
| ClearCache | Done | Done | clearCache |
| ClearChargingProfile | Done | Done | clearChargingProfile |
| DataTransfer | Done | Done | dataTransfer |
| DiagnosticsStatusNotification | Done | Done | diagnosticsStatusNotification |
| FirmwareStatusNotification | Done | Done | firmwareStatusNotification |
| GetCompositeSchedule | Done | Done | getCompositeSchedule |
| GetConfiguration | Done | Done | getConfiguration |
| GetDiagnostics | Done | Done | getDiagnostics |
| GetLocalListVersion | Done | Done | getLocalListVersion |
| Heartbeat | Done | Done | heartbeat |
| MeterValues | Done | Done | meterValues |
| RemoteStartTransaction | Done | Done | remoteStartTransaction |
| RemoteStopTransaction | Done | Done | remoteStopTransaction |
| ReserveNow | Done | Done | reserveNow |
| Reset | Done | Done | reset |
| SendLocalList | Done | Done | sendLocalList |
| SetChargingProfile | Done | Done | setChargingProfile |
| StartTransaction | Done | Done | startTransaction |
| StatusNotification | Done | Done | statusNotification |
| StopTransaction | Done | Done | stopTransaction |
| TriggerMessage | Done | Done | triggerMessage |
| UnlockConnector | Done | Done | unlockConnector |
| UpdateFirmware | Done | Done | updateFirmware |
- OCPP Naming - Messages use
Req()/Conf()to match OCPP terminology - Constructor Validation - All types require constructors that validate input
- Input Struct Pattern - Raw values passed via
ReqInput/ConfInputstructs, validated automatically - Immutability - Types use private fields and value receivers
- Error Accumulation - Constructors report all validation errors at once
using
errors.Join() - Error Wrapping - Context preserved via
fmt.Errorfwith%w - No Panics - Library never panics; all errors returned
- Thread Safety - Designed for safe concurrent use
- Go Conventions - Follows Effective Go guidelines
Security is critical for EV charging infrastructure. This library:
- Validates all input at construction time
- Prevents injection attacks via strict type constraints
- Provides clear error messages without exposing internals
- Uses immutable types to prevent tampering
- Is designed for safe concurrent use
Reporting vulnerabilities: See SECURITY.md for our security policy and responsible disclosure process.
We welcome contributions! Please:
- Follow Go best practices and Effective Go
- Add tests for all new functionality
- Ensure
make test-allpasses - Run
make lintandmake formatbefore committing - Document all exported types and functions
- Follow the existing code style
See CLAUDE.md for detailed development guidelines.
See LICENSE
- OCPP 1.6 Specification
- Go Package Documentation
- Security Policy
- Compatibility Contract
- Development Guide
- Contributing
- Roadmap
- Releasing
Condensed result:
PrimitiveDirectis fastest, but it skips safety checks.Customis slower in microbenchmarks, and the overhead is bounded and predictable versusPrimitiveValidated.- For OCPP workloads, correctness and developer ergonomics generally justify the cost of first-class datatypes.
Benchmark suite structure:
analysis_benchmak/bench_micro_types_test.go: constructor-level comparisons (DateTime,ParentIdTagchain, read paths).analysis_benchmak/bench_macro_messages_test.go: end-to-end message constructor path (StartTransactionReq).analysis_benchmak/bench_scaling_sendlocallist_test.go: slice-heavy scaling (SendLocalListReq) at1, 25, 100, 250, 500, 1000.analysis_benchmak/bench_getconfiguration_scaling_test.go: key-list scaling (GetConfigurationReq) at1, 25, 100, 250, 500, 1000.analysis_benchmak/bench_common_test.go: shared primitive baselines and validation helpers to keep comparisons consistent.
Single summary chart (Custom / PrimitiveValidated, lower is better):
Full benchmark report with 6 charts and detailed analysis: docs/benchmark.md
Reproduce:
go run ./scripts/benchreport.go