Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,32 @@ Requires Go 1.25+.
### CSMS (central system)

```go
srv := csms.NewServer(csms.WithSubProtocols("ocpp2.1", "ocpp2.0.1", "ocpp1.6"))
import v16h "github.com/shiv3/gocpp/v16/handlers"

csms.On(srv, v16p.BootNotification, func(ctx context.Context, c *csms.Conn, req v16msg.BootNotificationRequest) (v16msg.BootNotificationResponse, error) {
// 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
})

log.Fatal(srv.ListenAndServe(":8080")) // ws://host:8080/ocpp/{cpId}
}

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
Expand Down
163 changes: 87 additions & 76 deletions examples/csms-full/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ import (
"github.com/shiv3/gocpp/core/storage/memory"
"github.com/shiv3/gocpp/csms"
v16client "github.com/shiv3/gocpp/v16/client"
v16h "github.com/shiv3/gocpp/v16/handlers"
v16msg "github.com/shiv3/gocpp/v16/messages"
v16p "github.com/shiv3/gocpp/v16/profiles"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
Expand Down Expand Up @@ -194,6 +194,11 @@ func setupOTel(ctx context.Context) (trace.TracerProvider, observability.Metrics
}

type app struct {
// Embedding UnimplementedCSMSHandler makes *app a complete v16h.CSMSHandler:
// the messages whose On* methods are defined below are handled, and every
// other CP->CSMS message returns a NotSupported CallError.
v16h.UnimplementedCSMSHandler

logger *slog.Logger
srv *csms.Server
txStore storage.TransactionStore
Expand All @@ -203,84 +208,90 @@ type app struct {
txCounter atomic.Int32
}

// idTagAccepted/idTagInvalid are the canned authorization results shared by the
// Authorize/StartTransaction/StopTransaction handlers.
var (
idTagAccepted = v16msg.IDTagInfo{Status: v16msg.IDTagInfoStatusAccepted}
idTagInvalid = v16msg.IDTagInfo{Status: v16msg.IDTagInfoStatusInvalid}
)

// registerHandlers wires every CP->CSMS handler *app implements in one call.
// ChangeConfiguration/GetConfiguration are SentByCSMS (CSMS -> CP); the CSMS
// *sends* those (via v16client.NewCSMS(conn)), so they are not handlers here.
func (a *app) registerHandlers() {
accepted := v16msg.IDTagInfo{Status: v16msg.IDTagInfoStatusAccepted}
invalid := v16msg.IDTagInfo{Status: v16msg.IDTagInfoStatusInvalid}
must(v16h.RegisterCSMS(a.srv, a))
}

must(csms.On(a.srv, v16p.BootNotification, func(ctx context.Context, c *csms.Conn, req v16msg.BootNotificationRequest) (v16msg.BootNotificationResponse, error) {
a.logger.Info("BootNotification", "cp", c.ID(), "vendor", req.ChargePointVendor, "model", req.ChargePointModel)
if a.autoRemoteStart {
go a.driveRemoteStart(c)
}
return v16msg.BootNotificationResponse{CurrentTime: time.Now().UTC(), Interval: 300, Status: v16msg.RegistrationStatusAccepted}, nil
}))

must(csms.On(a.srv, v16p.Heartbeat, func(ctx context.Context, c *csms.Conn, req v16msg.HeartbeatRequest) (v16msg.HeartbeatResponse, error) {
a.logger.Info("Heartbeat", "cp", c.ID())
return v16msg.HeartbeatResponse{CurrentTime: time.Now().UTC()}, nil
}))

must(csms.On(a.srv, v16p.StatusNotification, func(ctx context.Context, c *csms.Conn, req v16msg.StatusNotificationRequest) (v16msg.StatusNotificationResponse, error) {
a.logger.Info("StatusNotification", "cp", c.ID(), "connector", req.ConnectorID, "status", req.Status)
return v16msg.StatusNotificationResponse{}, nil
}))

must(csms.On(a.srv, v16p.Authorize, func(ctx context.Context, c *csms.Conn, req v16msg.AuthorizeRequest) (v16msg.AuthorizeResponse, error) {
a.logger.Info("Authorize", "cp", c.ID(), "idTag", req.IDTag)
if !knownIDTag(req.IDTag) {
return v16msg.AuthorizeResponse{IDTagInfo: invalid}, nil
}
return v16msg.AuthorizeResponse{IDTagInfo: accepted}, nil
}))

must(csms.On(a.srv, v16p.StartTransaction, func(ctx context.Context, c *csms.Conn, req v16msg.StartTransactionRequest) (v16msg.StartTransactionResponse, error) {
txID := a.txCounter.Add(1)
err := a.txStore.Begin(ctx, storage.Transaction{
ID: itoa(txID),
CPID: c.ID(),
EVSEID: int(req.ConnectorID),
IDTag: req.IDTag,
StartedAt: time.Now().UTC(),
MeterStart: int(req.MeterStart),
Status: storage.TransactionActive,
})
if err != nil {
a.logger.Error("tx begin", "err", err)
}
a.logger.Info("StartTransaction", "cp", c.ID(), "tx", txID, "idTag", req.IDTag, "meterStart", req.MeterStart)
return v16msg.StartTransactionResponse{TransactionID: txID, IDTagInfo: accepted}, nil
}))
func (a *app) OnBootNotification(ctx context.Context, c *csms.Conn, req v16msg.BootNotificationRequest) (v16msg.BootNotificationResponse, error) {
a.logger.Info("BootNotification", "cp", c.ID(), "vendor", req.ChargePointVendor, "model", req.ChargePointModel)
if a.autoRemoteStart {
go a.driveRemoteStart(c)
}
return v16msg.BootNotificationResponse{CurrentTime: time.Now().UTC(), Interval: 300, Status: v16msg.RegistrationStatusAccepted}, nil
}

must(csms.On(a.srv, v16p.MeterValues, func(ctx context.Context, c *csms.Conn, req v16msg.MeterValuesRequest) (v16msg.MeterValuesResponse, error) {
if req.TransactionID != nil && len(req.MeterValue) > 0 {
a.logger.Info("MeterValues", "cp", c.ID(), "tx", *req.TransactionID, "samples", len(req.MeterValue))
}
return v16msg.MeterValuesResponse{}, nil
}))

must(csms.On(a.srv, v16p.StopTransaction, func(ctx context.Context, c *csms.Conn, req v16msg.StopTransactionRequest) (v16msg.StopTransactionResponse, error) {
txID := itoa(req.TransactionID)
_ = a.txStore.End(ctx, txID, storage.TransactionEnd{EndedAt: time.Now().UTC(), MeterStop: int(req.MeterStop), Status: storage.TransactionCompleted})
a.logger.Info("StopTransaction", "cp", c.ID(), "tx", req.TransactionID, "meterStop", req.MeterStop)
return v16msg.StopTransactionResponse{IDTagInfo: &accepted}, nil
}))

must(csms.On(a.srv, v16p.DataTransfer, func(ctx context.Context, c *csms.Conn, req v16msg.DataTransferRequest) (v16msg.DataTransferResponse, error) {
return v16msg.DataTransferResponse{Status: "Accepted"}, nil
}))

must(csms.On(a.srv, v16p.DiagnosticsStatusNotification, func(ctx context.Context, c *csms.Conn, req v16msg.DiagnosticsStatusNotificationRequest) (v16msg.DiagnosticsStatusNotificationResponse, error) {
a.logger.Info("DiagnosticsStatusNotification", "cp", c.ID(), "status", req.Status)
return v16msg.DiagnosticsStatusNotificationResponse{}, nil
}))

must(csms.On(a.srv, v16p.FirmwareStatusNotification, func(ctx context.Context, c *csms.Conn, req v16msg.FirmwareStatusNotificationRequest) (v16msg.FirmwareStatusNotificationResponse, error) {
a.logger.Info("FirmwareStatusNotification", "cp", c.ID(), "status", req.Status)
return v16msg.FirmwareStatusNotificationResponse{}, nil
}))
// Note: ChangeConfiguration/GetConfiguration are SentByCSMS (CSMS -> CP); the CSMS
// *sends* them (via v16client.NewCSMS(conn)), so they are not registered as
// inbound handlers here.
func (a *app) OnHeartbeat(ctx context.Context, c *csms.Conn, req v16msg.HeartbeatRequest) (v16msg.HeartbeatResponse, error) {
a.logger.Info("Heartbeat", "cp", c.ID())
return v16msg.HeartbeatResponse{CurrentTime: time.Now().UTC()}, nil
}

func (a *app) OnStatusNotification(ctx context.Context, c *csms.Conn, req v16msg.StatusNotificationRequest) (v16msg.StatusNotificationResponse, error) {
a.logger.Info("StatusNotification", "cp", c.ID(), "connector", req.ConnectorID, "status", req.Status)
return v16msg.StatusNotificationResponse{}, nil
}

func (a *app) OnAuthorize(ctx context.Context, c *csms.Conn, req v16msg.AuthorizeRequest) (v16msg.AuthorizeResponse, error) {
a.logger.Info("Authorize", "cp", c.ID(), "idTag", req.IDTag)
if !knownIDTag(req.IDTag) {
return v16msg.AuthorizeResponse{IDTagInfo: idTagInvalid}, nil
}
return v16msg.AuthorizeResponse{IDTagInfo: idTagAccepted}, nil
}

func (a *app) OnStartTransaction(ctx context.Context, c *csms.Conn, req v16msg.StartTransactionRequest) (v16msg.StartTransactionResponse, error) {
txID := a.txCounter.Add(1)
err := a.txStore.Begin(ctx, storage.Transaction{
ID: itoa(txID),
CPID: c.ID(),
EVSEID: int(req.ConnectorID),
IDTag: req.IDTag,
StartedAt: time.Now().UTC(),
MeterStart: int(req.MeterStart),
Status: storage.TransactionActive,
})
if err != nil {
a.logger.Error("tx begin", "err", err)
}
a.logger.Info("StartTransaction", "cp", c.ID(), "tx", txID, "idTag", req.IDTag, "meterStart", req.MeterStart)
return v16msg.StartTransactionResponse{TransactionID: txID, IDTagInfo: idTagAccepted}, nil
}

func (a *app) OnMeterValues(ctx context.Context, c *csms.Conn, req v16msg.MeterValuesRequest) (v16msg.MeterValuesResponse, error) {
if req.TransactionID != nil && len(req.MeterValue) > 0 {
a.logger.Info("MeterValues", "cp", c.ID(), "tx", *req.TransactionID, "samples", len(req.MeterValue))
}
return v16msg.MeterValuesResponse{}, nil
}

func (a *app) OnStopTransaction(ctx context.Context, c *csms.Conn, req v16msg.StopTransactionRequest) (v16msg.StopTransactionResponse, error) {
txID := itoa(req.TransactionID)
_ = a.txStore.End(ctx, txID, storage.TransactionEnd{EndedAt: time.Now().UTC(), MeterStop: int(req.MeterStop), Status: storage.TransactionCompleted})
a.logger.Info("StopTransaction", "cp", c.ID(), "tx", req.TransactionID, "meterStop", req.MeterStop)
return v16msg.StopTransactionResponse{IDTagInfo: &idTagAccepted}, nil
}

func (a *app) OnDataTransfer(ctx context.Context, c *csms.Conn, req v16msg.DataTransferRequest) (v16msg.DataTransferResponse, error) {
return v16msg.DataTransferResponse{Status: "Accepted"}, nil
}

func (a *app) OnDiagnosticsStatusNotification(ctx context.Context, c *csms.Conn, req v16msg.DiagnosticsStatusNotificationRequest) (v16msg.DiagnosticsStatusNotificationResponse, error) {
a.logger.Info("DiagnosticsStatusNotification", "cp", c.ID(), "status", req.Status)
return v16msg.DiagnosticsStatusNotificationResponse{}, nil
}

func (a *app) OnFirmwareStatusNotification(ctx context.Context, c *csms.Conn, req v16msg.FirmwareStatusNotificationRequest) (v16msg.FirmwareStatusNotificationResponse, error) {
a.logger.Info("FirmwareStatusNotification", "cp", c.ID(), "status", req.Status)
return v16msg.FirmwareStatusNotificationResponse{}, nil
}

func knownIDTag(idTag string) bool {
Expand Down
26 changes: 16 additions & 10 deletions examples/csms-minimal/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,31 @@ import (
"time"

"github.com/shiv3/gocpp/csms"
v16h "github.com/shiv3/gocpp/v16/handlers"
v16msg "github.com/shiv3/gocpp/v16/messages"
v16p "github.com/shiv3/gocpp/v16/profiles"
)

// handler implements the CP->CSMS messages this server cares about. Embedding
// UnimplementedCSMSHandler makes every other message return a NotSupported
// CallError automatically.
type handler struct{ v16h.UnimplementedCSMSHandler }

func (handler) OnBootNotification(ctx context.Context, c *csms.Conn, req v16msg.BootNotificationRequest) (v16msg.BootNotificationResponse, error) {
slog.Info("boot", "cp", c.ID(), "vendor", req.ChargePointVendor)
return v16msg.BootNotificationResponse{Status: v16msg.RegistrationStatusAccepted, CurrentTime: time.Now(), Interval: 300}, nil
}

func (handler) OnHeartbeat(ctx context.Context, c *csms.Conn, req v16msg.HeartbeatRequest) (v16msg.HeartbeatResponse, error) {
return v16msg.HeartbeatResponse{CurrentTime: time.Now()}, nil
}

func main() {
srv := csms.NewServer(
csms.WithSubProtocols("ocpp1.6"),
csms.WithLogger(slog.Default()),
)

if err := csms.On(srv, v16p.BootNotification, func(ctx context.Context, c *csms.Conn, req v16msg.BootNotificationRequest) (v16msg.BootNotificationResponse, error) {
slog.Info("boot", "cp", c.ID(), "vendor", req.ChargePointVendor)
return v16msg.BootNotificationResponse{Status: v16msg.RegistrationStatusAccepted, CurrentTime: time.Now(), Interval: 300}, nil
}); err != nil {
log.Fatal(err)
}
if err := csms.On(srv, v16p.Heartbeat, func(ctx context.Context, c *csms.Conn, req v16msg.HeartbeatRequest) (v16msg.HeartbeatResponse, error) {
return v16msg.HeartbeatResponse{CurrentTime: time.Now()}, nil
}); err != nil {
if err := v16h.RegisterCSMS(srv, handler{}); err != nil {
log.Fatal(err)
}

Expand Down
34 changes: 19 additions & 15 deletions examples/migration-after/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,34 @@ import (
"time"

"github.com/shiv3/gocpp/csms"
v16h "github.com/shiv3/gocpp/v16/handlers"
v16msg "github.com/shiv3/gocpp/v16/messages"
v16p "github.com/shiv3/gocpp/v16/profiles"
)

// handler mirrors ocpp-go's CentralSystemHandler interface: implement the
// messages you care about and embed UnimplementedCSMSHandler for the rest.
type handler struct{ v16h.UnimplementedCSMSHandler }

func (handler) OnBootNotification(ctx context.Context, c *csms.Conn, req v16msg.BootNotificationRequest) (v16msg.BootNotificationResponse, error) {
slog.Info("boot", "cp", c.ID(), "vendor", req.ChargePointVendor)
return v16msg.BootNotificationResponse{
Status: v16msg.RegistrationStatusAccepted,
CurrentTime: time.Now(),
Interval: 300,
}, nil
}

func (handler) OnHeartbeat(ctx context.Context, c *csms.Conn, req v16msg.HeartbeatRequest) (v16msg.HeartbeatResponse, error) {
return v16msg.HeartbeatResponse{CurrentTime: time.Now()}, nil
}

func main() {
srv := csms.NewServer(
csms.WithSubProtocols("ocpp1.6"),
csms.WithLogger(slog.Default()),
)

if err := csms.On(srv, v16p.BootNotification, func(ctx context.Context, c *csms.Conn, req v16msg.BootNotificationRequest) (v16msg.BootNotificationResponse, error) {
slog.Info("boot", "cp", c.ID(), "vendor", req.ChargePointVendor)
return v16msg.BootNotificationResponse{
Status: v16msg.RegistrationStatusAccepted,
CurrentTime: time.Now(),
Interval: 300,
}, nil
}); err != nil {
log.Fatal(err)
}

if err := csms.On(srv, v16p.Heartbeat, func(ctx context.Context, c *csms.Conn, req v16msg.HeartbeatRequest) (v16msg.HeartbeatResponse, error) {
return v16msg.HeartbeatResponse{CurrentTime: time.Now()}, nil
}); err != nil {
if err := v16h.RegisterCSMS(srv, handler{}); err != nil {
log.Fatal(err)
}

Expand Down