Documentation
¶
Overview ¶
Package csms provides an OCPP-J Central System Management System server.
Server owns WebSocket connections and session lifecycles. Router dispatches inbound CALL messages to application-provided typed handlers, while Call sends typed requests from the CSMS to a connected Charging Station.
Index ¶
- Variables
- func Call[Request protocol.Payload, Confirmation protocol.Payload](ctx context.Context, session *Session, request Request) (Confirmation, error)
- func Handle[Request protocol.Payload, Confirmation protocol.Payload](router *Router, handler TypedHandler[Request, Confirmation]) error
- func HandleAfter[Request protocol.Payload, Confirmation protocol.Payload](router *Router, handler TypedAfterHandler[Request, Confirmation]) error
- func HandleSend[Request protocol.Payload](router *Router, handler TypedSendHandler[Request]) error
- func ValidateIdentity(identity string) error
- type AfterHandler
- type AuthenticationRequest
- type Authenticator
- type AuthenticatorFunc
- type BasicCredentials
- type CallError
- type Config
- type DuplicateSessionAttempt
- type DuplicateSessionDecision
- type DuplicateSessionHandler
- type ErrorCode
- type Handler
- type HandshakeAttempt
- type HandshakeLimiter
- type HandshakeLimiterFunc
- type IPRateLimiter
- type IdentityValidator
- type LogEvent
- type LogLevel
- type LogRecord
- type Logger
- type LoggerFunc
- type MetricEvent
- type MetricEventType
- type Metrics
- type MetricsFunc
- type Middleware
- type Principal
- type RemoteCallError
- type Router
- func (r *Router) Handle(version protocol.Version, action string, handler Handler) error
- func (r *Router) HandleAfter(version protocol.Version, action string, handler AfterHandler) error
- func (r *Router) HandleSend(version protocol.Version, action string, handler Handler) error
- func (r *Router) Use(middleware Middleware)
- type SecurityConfig
- type SecurityEvent
- type SecurityEventHandler
- type SecurityEventType
- type SecurityProfile
- type Server
- func (s *Server) Healthy() bool
- func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)
- func (s *Server) Session(identity string) (*Session, bool)
- func (s *Server) SessionCount() int
- func (s *Server) Sessions() []*Session
- func (s *Server) Shutdown(ctx context.Context) error
- func (s *Server) Snapshot() ServerSnapshot
- type ServerSnapshot
- type Session
- func (s *Session) Close() error
- func (s *Session) Context() context.Context
- func (s *Session) Done() <-chan struct{}
- func (s *Session) Err() error
- func (s *Session) Identity() string
- func (s *Session) Info() SessionInfo
- func (s *Session) Principal() Principal
- func (s *Session) Version() protocol.Version
- type SessionInfo
- type SessionState
- type TypedAfterHandler
- type TypedHandler
- type TypedSendHandler
- type UniqueIDGenerator
Constants ¶
This section is empty.
Variables ¶
var ( ErrInvalidHandlerRegistration = errors.New("invalid OCPP handler registration") ErrHandlerAlreadyRegistered = errors.New("OCPP handler is already registered") )
var ( ErrAuthenticationFailed = errors.New("authentication failed") )
var ( ErrInvalidConfiguration = errors.New("invalid CSMS configuration") ErrUniqueIDGeneration = errors.New("OCPP unique ID generation failed") )
var ( ErrSessionClosed = errors.New("OCPP session is closed") ErrSessionReplaced = errors.New("OCPP session was replaced") ErrServerShutdown = errors.New("OCPP server is shutting down") ErrPongTimeout = errors.New("WebSocket pong timeout") ErrIdleTimeout = errors.New("OCPP session idle timeout") ErrTooManyPendingCalls = errors.New("too many pending OCPP calls") ErrDuplicateUniqueID = errors.New("duplicate OCPP unique ID") ErrHandlerQueueFull = errors.New("OCPP handler queue is full") )
Functions ¶
func Call ¶
func Call[Request protocol.Payload, Confirmation protocol.Payload]( ctx context.Context, session *Session, request Request, ) (Confirmation, error)
Call sends a generated request to a Charging Station and waits for the matching generated confirmation or CALLERROR.
func Handle ¶
func Handle[Request protocol.Payload, Confirmation protocol.Payload]( router *Router, handler TypedHandler[Request, Confirmation], ) error
Handle registers a type-safe OCPP handler. Request and Confirmation must be generated non-pointer payload types for the same action and version.
func HandleAfter ¶ added in v0.2.0
func HandleAfter[Request protocol.Payload, Confirmation protocol.Payload]( router *Router, handler TypedAfterHandler[Request, Confirmation], ) error
HandleAfter registers a type-safe post-response callback. Multiple callbacks for the same action are allowed and run in registration order.
func HandleSend ¶
func HandleSend[Request protocol.Payload](router *Router, handler TypedSendHandler[Request]) error
HandleSend registers an OCPP 2.1 SEND handler. SEND is unconfirmed, so an error returned by the handler is not written back to the Charging Station.
func ValidateIdentity ¶
ValidateIdentity accepts RFC 3986 unreserved characters and limits an OCPP identity to 255 bytes. Applications may replace it in SecurityConfig.
Types ¶
type AfterHandler ¶ added in v0.2.0
AfterHandler runs after a CALLRESULT has been written successfully. The request is the original CALL payload and confirmation is the value returned by the main handler. Its error is diagnostic only because the peer has already received the successful response.
type AuthenticationRequest ¶
type AuthenticationRequest struct {
Identity string
Version protocol.Version
RemoteAddr string
TLS *tls.ConnectionState
Basic *BasicCredentials
}
type Authenticator ¶
type Authenticator interface {
Authenticate(context.Context, AuthenticationRequest) (Principal, error)
}
type AuthenticatorFunc ¶
type AuthenticatorFunc func(context.Context, AuthenticationRequest) (Principal, error)
func (AuthenticatorFunc) Authenticate ¶
func (function AuthenticatorFunc) Authenticate(ctx context.Context, request AuthenticationRequest) (Principal, error)
type BasicCredentials ¶
type Config ¶
type Config struct {
Router *Router
Versions []protocol.Version
ReadLimit int64
HandshakeTimeout time.Duration
CallTimeout time.Duration
MaxPendingCalls int
MaxConcurrentHandlers int
// MaxQueuedHandlers bounds how many inbound CALL/SEND messages may be
// admitted (running or waiting for a free MaxConcurrentHandlers slot)
// at once, per session. Default: 4x MaxConcurrentHandlers. Once full, a
// further inbound CALL gets an immediate CALLERROR (GenericError) and a
// further inbound SEND is logged and dropped, instead of either
// blocking the read loop (which would also stall reading this
// session's own outbound Call responses) or admitting an unbounded
// number of goroutines waiting for a slot.
MaxQueuedHandlers int
UniqueIDGenerator UniqueIDGenerator
OnDuplicateSession DuplicateSessionHandler
WriteTimeout time.Duration
PingInterval time.Duration
PongTimeout time.Duration
IdleTimeout time.Duration
CheckOrigin func(*http.Request) bool
OnConnect func(*Session)
OnDisconnect func(*Session, error)
Logger Logger
Metrics Metrics
Security SecurityConfig
}
Config controls a CSMS Server. Construct it with keyed fields: new optional fields may be added in backward-compatible releases. Zero values select the documented default except IdleTimeout, where zero disables idle detection. Negative durations and limits are rejected.
type DuplicateSessionAttempt ¶
type DuplicateSessionAttempt struct {
Identity string
Existing SessionInfo
Request *http.Request
}
type DuplicateSessionDecision ¶
type DuplicateSessionDecision uint8
const ( RejectNewSession DuplicateSessionDecision = iota + 1 ReplaceExistingSession )
type DuplicateSessionHandler ¶
type DuplicateSessionHandler func(context.Context, DuplicateSessionAttempt) (DuplicateSessionDecision, error)
type ErrorCode ¶
type ErrorCode string
const ( NotImplemented ErrorCode = "NotImplemented" NotSupported ErrorCode = "NotSupported" FormationViolation ErrorCode = "FormationViolation" FormatViolation ErrorCode = "FormatViolation" InternalError ErrorCode = "InternalError" ProtocolError ErrorCode = "ProtocolError" SecurityError ErrorCode = "SecurityError" PropertyConstraintViolation ErrorCode = "PropertyConstraintViolation" OccurenceConstraintViolation ErrorCode = "OccurenceConstraintViolation" OccurrenceConstraintViolation ErrorCode = "OccurrenceConstraintViolation" TypeConstraintViolation ErrorCode = "TypeConstraintViolation" MessageTypeNotSupported ErrorCode = "MessageTypeNotSupported" RpcFrameworkError ErrorCode = "RpcFrameworkError" GenericError ErrorCode = "GenericError" )
type HandshakeAttempt ¶
type HandshakeLimiter ¶
type HandshakeLimiter interface {
Allow(context.Context, HandshakeAttempt) bool
}
type HandshakeLimiterFunc ¶
type HandshakeLimiterFunc func(context.Context, HandshakeAttempt) bool
func (HandshakeLimiterFunc) Allow ¶
func (function HandshakeLimiterFunc) Allow(ctx context.Context, attempt HandshakeAttempt) bool
type IPRateLimiter ¶
type IPRateLimiter struct {
// contains filtered or unexported fields
}
IPRateLimiter limits WebSocket handshake attempts per remote IP using fixed windows. It is safe for concurrent use.
func NewIPRateLimiter ¶
func NewIPRateLimiter(limit int, window time.Duration) (*IPRateLimiter, error)
func (*IPRateLimiter) Allow ¶
func (limiter *IPRateLimiter) Allow(_ context.Context, attempt HandshakeAttempt) bool
type IdentityValidator ¶
type LogEvent ¶
type LogEvent string
const ( LogSessionConnected LogEvent = "session.connected" LogSessionDisconnected LogEvent = "session.disconnected" LogCallReceived LogEvent = "call.received" LogCallCompleted LogEvent = "call.completed" LogCallRejected LogEvent = "call.rejected" LogCallAfterFailed LogEvent = "call.after_failed" LogSendReceived LogEvent = "send.received" LogSendCompleted LogEvent = "send.completed" LogSendDropped LogEvent = "send.dropped" )
type LogRecord ¶
type LogRecord struct {
Level LogLevel
Event LogEvent
Identity string
Version protocol.Version
MessageType protocol.MessageTypeID
MessageID string
Action string
ErrorCode ErrorCode
Reason string
}
LogRecord contains protocol metadata only. Payloads, credentials, certificates, idTokens and handler error text are intentionally omitted.
type LoggerFunc ¶
type MetricEvent ¶ added in v0.2.0
type MetricEvent struct {
Type MetricEventType
// Identity is the charging station's identity string. It is NOT safe to
// use directly as a label in a cardinality-sensitive metrics backend
// (for example a Prometheus label) in a large deployment: every
// distinct Identity creates a new time series, and a fleet of
// thousands of charging stations produces thousands of series per
// metric — a well-known Prometheus operational failure mode. This is a
// deployment-scale judgment call the library cannot make correctly for
// every user (fine for a 10-charger depot, wrong for a 50,000-charger
// public network), so it is left entirely to the Metrics
// implementation: use Identity for filtering/branching logic (e.g.
// route a subset of chargers to more detailed stats) rather than
// passing it straight through as a label, unless your deployment is
// small enough that per-charger series are acceptable. See
// examples/prometheus-hook for a wiring example that deliberately
// excludes Identity from its labels.
Identity string
Version protocol.Version
MessageType protocol.MessageTypeID
Action string
Duration time.Duration
ErrorCode ErrorCode
}
MetricEvent describes one measurable protocol event. Unlike LogRecord, it is not meant for human-readable diagnostics: it carries Duration for latency observation and has no free-form reason text, so implementations can use Type/Action/Version/ErrorCode directly as low-cardinality metric labels. Identity is deliberately not in that list — see its own comment.
type MetricEventType ¶ added in v0.2.0
type MetricEventType uint8
const ( // MetricSessionConnected is recorded as early as possible in the // connection lifecycle, before the read loop starts and before // Config.OnConnect runs. It is still not guaranteed to be the first // event Metrics.Record observes for a session: dispatch is async and // unordered (see the Metrics doc comment), so a Metrics implementation // must not assume Connected always precedes every other event for the // same identity. MetricSessionConnected MetricEventType = iota + 1 // MetricSessionDisconnected carries Duration equal to the session's // total lifetime (time since connect). MetricSessionDisconnected MetricCallReceived // MetricCallCompleted carries Duration for validation, handler // execution, and the CALLRESULT write. MetricCallCompleted // MetricCallRejected carries Duration and ErrorCode. MetricCallRejected MetricSendReceived MetricSendCompleted MetricSendDropped // MetricOutboundCallRejected fires when Call is rejected before a CALL // frame is ever sent because MaxPendingCalls was reached. Duration is // always zero. MetricOutboundCallRejected // MetricOutboundCallSent fires once the CALL frame has been written. // Duration is always zero. MetricOutboundCallSent // MetricOutboundCallCompleted carries Duration measured from just after // the call was admitted into the pending-call table (i.e. it includes // CALL marshal/write time, not strictly the interval after // MetricOutboundCallSent) to a valid CALLRESULT. MetricOutboundCallCompleted // MetricOutboundCallFailed covers a remote CALLERROR, an undecodable or // invalid confirmation, a transport write failure unrelated to context // cancellation, and the session closing while the call was pending. // ErrorCode is set only for a remote CALLERROR. Note: this also fires // for every pending call during an ordinary graceful Server.Shutdown // (the session closes with ErrServerShutdown), which is expected and // not a regression. MetricOutboundCallFailed // MetricOutboundCallTimeout fires when the call's context deadline // expires, including when that deadline has already passed at the // moment the CALL frame is written. It cannot distinguish // Config.CallTimeout from a deadline the caller supplied on its own // context. MetricOutboundCallTimeout // MetricOutboundCallCanceled fires when the caller cancels its own // context while the call is pending (including if it was already // canceled at the moment the CALL frame is written). This is not a // failure. Caveat: context.Context only exposes context.Canceled, not // why a context was canceled, so if a caller ties Call's ctx to // Session.Context() (or another context whose cancellation is driven by // the session's own lifecycle rather than the caller's own decision), // the session closing for a reason unrelated to the call itself // (Server.Shutdown, an idle/pong timeout, being replaced) is also // reported here rather than as MetricOutboundCallFailed — this hook // cannot distinguish the two cases from ctx.Err() alone. MetricOutboundCallCanceled )
type Metrics ¶ added in v0.2.0
type Metrics interface {
Record(context.Context, MetricEvent)
}
Metrics receives MetricEvent notifications for connection, CALL, and SEND lifecycle events. Implementations are expected to aggregate events (for example into counters and histograms) rather than perform I/O per event.
Record must be safe for concurrent use: a fixed worker pool shared by all sessions on a Server dispatches events, so Record may run concurrently with itself and carries no ordering guarantee relative to when events logically occurred. A slow or blocking Record implementation never delays the protocol server — recordMetric only attempts a non-blocking enqueue — but permanently blocking calls reduce the worker pool's effective capacity. If producers remain faster than the available workers and the bounded queue fills, further events are silently dropped rather than blocking.
The context passed to Record is always context.Background(), never a request- or session-scoped context: every MetricEvent can be produced at a point where the natural originating context (an inbound handler's context, an outbound Call's context, or the session's own context) is already canceled — most obviously for the very events that report a timeout, cancellation, or shutdown — and propagating it would let a defensively written Record implementation (one that no-ops when ctx.Err() != nil) silently drop exactly those events. A future tracing integration that needs span correlation will need its own mechanism rather than relying on this ctx.
type MetricsFunc ¶ added in v0.2.0
type MetricsFunc func(context.Context, MetricEvent)
func (MetricsFunc) Record ¶ added in v0.2.0
func (f MetricsFunc) Record(ctx context.Context, event MetricEvent)
type RemoteCallError ¶
type RemoteCallError struct {
Code ErrorCode
Description string
Details json.RawMessage
}
RemoteCallError is a CALLERROR returned by a Charging Station.
func (*RemoteCallError) Error ¶
func (e *RemoteCallError) Error() string
type Router ¶
type Router struct {
// contains filtered or unexported fields
}
func (*Router) Handle ¶
Handle registers a new CALL-answering handler for version and action. A registration is immutable: duplicate keys are rejected instead of silently replacing the existing handler. Only a CALL frame can reach a handler registered this way — use HandleSend to register a handler for SEND (fire-and-forget) frames instead.
func (*Router) HandleAfter ¶ added in v0.2.0
HandleAfter appends a callback that runs after a successful CALLRESULT write. An action may have multiple callbacks; they run in registration order.
func (*Router) HandleSend ¶ added in v0.2.0
HandleSend registers a new SEND (fire-and-forget) handler for version and action, following the same immutability rule as Handle. Only a SEND frame can reach a handler registered this way.
func (*Router) Use ¶
func (r *Router) Use(middleware Middleware)
Use appends middleware that is applied to every registered handler at lookup time. Middleware is executed in registration order.
type SecurityConfig ¶
type SecurityConfig struct {
Profile SecurityProfile
Authenticator Authenticator
ValidateIdentity IdentityValidator
AllowBasicUsernameMismatch bool
MinTLSVersion uint16
HandshakeLimiter HandshakeLimiter
OnEvent SecurityEventHandler
}
type SecurityEvent ¶
type SecurityEventHandler ¶
type SecurityEventHandler func(context.Context, SecurityEvent)
type SecurityEventType ¶
type SecurityEventType string
const ( SecurityEventAuthenticationSucceeded SecurityEventType = "AuthenticationSucceeded" SecurityEventAuthenticationFailed SecurityEventType = "AuthenticationFailed" SecurityEventHandshakeRateLimited SecurityEventType = "HandshakeRateLimited" SecurityEventDuplicateRejected SecurityEventType = "DuplicateSessionRejected" SecurityEventSessionReplaced SecurityEventType = "SessionReplaced" )
type SecurityProfile ¶
type SecurityProfile uint8
const ( SecurityProfileUnsecured SecurityProfile = iota SecurityProfileBasicAuth SecurityProfileTLSBasicAuth SecurityProfileTLSClientCertificate )
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
func (*Server) Healthy ¶ added in v0.2.0
Healthy reports whether the server is still accepting new connections. It becomes false as soon as Shutdown is called.
func (*Server) ServeHTTP ¶
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP accepts /{chargePointIdentity} and negotiates an OCPP subprotocol.
func (*Server) SessionCount ¶
func (*Server) Shutdown ¶
Shutdown rejects new connections, closes active sessions and waits until they finish or ctx expires, then stops this Server's metric dispatch workers (if Config.Metrics was set) and waits for them to actually exit too — Shutdown is terminal, a Server cannot accept new connections afterward, so it should honestly release everything it started, not leave metricDispatchWorkers goroutines running forever. Repeated calls are safe.
func (*Server) Snapshot ¶ added in v0.2.0
func (s *Server) Snapshot() ServerSnapshot
Snapshot returns the server's current status. It is safe to call from any goroutine and does not block on session I/O.
type ServerSnapshot ¶ added in v0.2.0
type ServerSnapshot struct {
ActiveSessions int
ShuttingDown bool
Sessions []SessionInfo
}
ServerSnapshot is a point-in-time view of server status, suitable for a health or readiness endpoint the application wires up itself.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session represents one server-owned Charging Station WebSocket connection. Applications may inspect it, close it, and pass it to Call or typed profile methods. Raw frame writes are intentionally not exposed.
func (*Session) Info ¶
func (s *Session) Info() SessionInfo
type SessionInfo ¶
type SessionState ¶
type SessionState uint32
const ( SessionActive SessionState = iota + 1 SessionClosing SessionClosed )
type TypedAfterHandler ¶ added in v0.2.0
type TypedAfterHandler[Request protocol.Payload, Confirmation protocol.Payload] func( context.Context, *Session, Request, Confirmation, ) error
TypedAfterHandler observes a successfully answered CALL after its CALLRESULT has been written to the peer.
type TypedHandler ¶
type TypedHandler[Request protocol.Payload, Confirmation protocol.Payload] func( context.Context, *Session, Request, ) (Confirmation, error)
TypedHandler receives a schema-validated request and returns a confirmation of the same OCPP version and action.
type TypedSendHandler ¶
type UniqueIDGenerator ¶
type UniqueIDGenerator func() string
UniqueIDGenerator returns an OCPP-J unique ID of 1 to 36 characters. Panics, empty IDs, overlong IDs, and duplicate in-flight IDs are converted to errors by Call.