项目作者: lorenzodonini

项目描述 :
Open Charge Point Protocol implementation in Go
高级语言: Go
项目地址: git://github.com/lorenzodonini/ocpp-go.git
创建时间: 2019-04-19T20:49:36Z
项目社区:https://github.com/lorenzodonini/ocpp-go

开源协议:MIT License

下载


ocpp-go

Build Status
GoDoc
Coverage Status
Go report

Open Charge Point Protocol implementation in Go.

The library targets modern charge points and central systems, running OCPP version 1.6+.

Given that SOAP will no longer be supported in future versions of OCPP, only OCPP-J is supported in this library.
There are currently no plans of supporting OCPP-S.

Status & Roadmap

Note: Releases 0.10.0 introduced breaking changes in some API, due to refactoring. The functionality remains the same,
but naming changed.

Planned milestones and features:

  • OCPP 1.6
  • OCPP 1.6 Security extension (experimental)
  • OCPP 2.0.1 (examples working, but will need more real-world testing)
  • Dedicated package for configuration management

OCPP 1.6 Usage

Go version 1.13+ is required.

  1. go get github.com/lorenzodonini/ocpp-go

You will also need to fetch some dependencies:

  1. cd <path-to-ocpp-go>
  2. export GO111MODULE=on
  3. go mod download

Your application may either act as a Central System (server) or as a Charge Point (
client).

Central System

If you want to integrate the library into your custom Central System, you must implement the callbacks defined in the
profile interfaces, e.g.:

  1. import (
  2. "github.com/lorenzodonini/ocpp-go/ocpp1.6/core"
  3. "github.com/lorenzodonini/ocpp-go/ocpp1.6/types"
  4. "time"
  5. )
  6. const defaultHeartbeatInterval = 600
  7. type CentralSystemHandler struct {
  8. // ... your own state variables
  9. }
  10. func (handler *CentralSystemHandler) OnAuthorize(chargePointId string, request *core.AuthorizeRequest) (confirmation *core.AuthorizeConfirmation, err error) {
  11. // ... your own custom logic
  12. return core.NewAuthorizationConfirmation(types.NewIdTagInfo(types.AuthorizationStatusAccepted)), nil
  13. }
  14. func (handler *CentralSystemHandler) OnBootNotification(chargePointId string, request *core.BootNotificationRequest) (confirmation *core.BootNotificationConfirmation, err error) {
  15. // ... your own custom logic
  16. return core.NewBootNotificationConfirmation(types.NewDateTime(time.Now()), defaultHeartbeatInterval, core.RegistrationStatusAccepted), nil
  17. }
  18. // further callbacks...

Every time a request from the charge point comes in, the respective callback function is called.
For every callback you must return either a confirmation or an error. The result will be sent back automatically to the
charge point.
The callback is invoked inside a dedicated goroutine, so you don’t have to worry about synchronization.

You need to implement at least all other callbacks defined in the core.CentralSystemHandler interface.

Depending on which OCPP profiles you want to support in your application, you will need to implement additional
callbacks as well.

To start a central system instance, simply run the following:

  1. centralSystem := ocpp16.NewCentralSystem(nil, nil)
  2. // Set callback handlers for connect/disconnect
  3. centralSystem.SetNewChargePointHandler(func (chargePointId string) {
  4. log.Printf("new charge point %v connected", chargePointId)
  5. })
  6. centralSystem.SetChargePointDisconnectedHandler(func (chargePointId string) {
  7. log.Printf("charge point %v disconnected", chargePointId)
  8. })
  9. // Set handler for profile callbacks
  10. handler := &CentralSystemHandler{}
  11. centralSystem.SetCoreHandler(handler)
  12. // Start central system
  13. listenPort := 8887
  14. log.Printf("starting central system")
  15. centralSystem.Start(listenPort, "/{ws}") // This call starts server in daemon mode and is blocking
  16. log.Println("stopped central system")

Sending requests

To send requests to the charge point, you may either use the simplified API:

  1. err := centralSystem.ChangeAvailability("1234", myCallback, 1, core.AvailabilityTypeInoperative)
  2. if err != nil {
  3. log.Printf("error sending message: %v", err)
  4. }

or create a message manually:

  1. request := core.NewChangeAvailabilityRequest(1, core.AvailabilityTypeInoperative)
  2. err := centralSystem.SendRequestAsync("clientId", request, callbackFunction)
  3. if err != nil {
  4. log.Printf("error sending message: %v", err)
  5. }

In both cases, the request is sent asynchronously and the function returns right away.
You need to write the callback function to check for errors and handle the confirmation on your own:

  1. myCallback := func (confirmation *core.ChangeAvailabilityConfirmation, e error) {
  2. if e != nil {
  3. log.Printf("operation failed: %v", e)
  4. } else {
  5. log.Printf("status: %v", confirmation.Status)
  6. // ... your own custom logic
  7. }
  8. }

Since the initial centralSystem.Start call blocks forever, you may want to wrap it in a goroutine (that is, if you
need to run other operations on the main thread).

Example

You can take a look at the full example.
To run it, simply execute:

  1. go run ./example/1.6/cs/*.go

Docker

A containerized version of the central system example is available:

  1. docker pull ldonini/ocpp1.6-central-system:latest
  2. docker run -it -p 8887:8887 --rm --name central-system ldonini/ocpp1.6-central-system:latest

You can also run it directly using docker-compose:

  1. docker-compose -f example/1.6/docker-compose.yml up central-system

TLS

If you wish to test the central system using TLS, make sure you put your self-signed certificates inside the
example/1.6/certs folder.

Feel free to use the utility script cd example/1.6 && ./create-test-certificates.sh for generating test certificates.

Then run the following:

  1. docker-compose -f example/1.6/docker-compose.tls.yml up central-system

Charge Point

If you want to integrate the library into your custom Charge Point, you must implement the callbacks defined in the
profile interfaces, e.g.:

  1. import (
  2. "github.com/lorenzodonini/ocpp-go/ocpp1.6/core"
  3. "github.com/lorenzodonini/ocpp-go/ocpp1.6/types"
  4. )
  5. type ChargePointHandler struct {
  6. // ... your own state variables
  7. }
  8. func (handler *ChargePointHandler) OnChangeAvailability(request *core.ChangeAvailabilityRequest) (confirmation *core.ChangeAvailabilityConfirmation, err error) {
  9. // ... your own custom logic
  10. return core.NewChangeAvailabilityConfirmation(core.AvailabilityStatusAccepted), nil
  11. }
  12. func (handler *ChargePointHandler) OnChangeConfiguration(request *core.ChangeConfigurationRequest) (confirmation *core.ChangeConfigurationConfirmation, err error) {
  13. // ... your own custom logic
  14. return core.NewChangeConfigurationConfirmation(core.ConfigurationStatusAccepted), nil
  15. }
  16. // further callbacks...

When a request from the central system comes in, the respective callback function gets invoked.
For every callback you must return either a confirmation or an error. The result will be sent back automatically to the
central system.

You need to implement at least all other callbacks defined in the core.ChargePointHandler interface.

Depending on which OCPP profiles you want to support in your application, you will need to implement additional
callbacks as well.

To start a charge point instance, simply run the following:

  1. chargePointId := "cp0001"
  2. csUrl = "ws://localhost:8887"
  3. chargePoint := ocpp16.NewChargePoint(chargePointId, nil, nil)
  4. // Set a handler for all callback functions
  5. handler := &ChargePointHandler{}
  6. chargePoint.SetCoreHandler(handler)
  7. // Connects to central system
  8. err := chargePoint.Start(csUrl)
  9. if err != nil {
  10. log.Println(err)
  11. } else {
  12. log.Printf("connected to central system at %v", csUrl)
  13. mainRoutine() // ... your program logic goes here
  14. }
  15. // Disconnect
  16. chargePoint.Stop()
  17. log.Printf("disconnected from central system")

Sending requests

To send requests to the central station, you have two options. You may either use the simplified synchronous blocking
API (recommended):

  1. bootConf, err := chargePoint.BootNotification("model1", "vendor1")
  2. if err != nil {
  3. log.Fatal(err)
  4. } else {
  5. log.Printf("status: %v, interval: %v, current time: %v", bootConf.Status, bootConf.Interval, bootConf.CurrentTime.String())
  6. }
  7. // ... do something with the confirmation

or create a message manually:

  1. request := core.NewBootNotificationRequest("model1", "vendor1")

You can then decide to send the message using a synchronous blocking call:

  1. // Synchronous call
  2. confirmation, err := chargePoint.SendRequest(request)
  3. if err != nil {
  4. log.Printf("error sending message: %v", err)
  5. }
  6. bootConf := confirmation.(*core.BootNotificationConfirmation)
  7. // ... do something with the confirmation

or an asynchronous call:

  1. // Asynchronous call
  2. err := chargePoint.SendRequestAsync(request, callbackFunction)
  3. if err != nil {
  4. log.Printf("error sending message: %v", err)
  5. }

In the latter case, you need to write the callback function and check for errors on your own:

  1. callback := func (confirmation ocpp.Response, e error) {
  2. bootConf := confirmation.(*core.BootNotificationConfirmation)
  3. if e != nil {
  4. log.Printf("operation failed: %v", e)
  5. } else {
  6. log.Printf("status: %v", bootConf.Status)
  7. // ... your own custom logic
  8. }
  9. }

When creating a message manually, you always need to perform type assertion yourself, as the SendRequest and
SendRequestAsync APIs use generic Request and Confirmation interfaces.

Example

You can take a look at the full example.
To run it, simply execute:

  1. CLIENT_ID=chargePointSim CENTRAL_SYSTEM_URL=ws://<host>:8887 go run example/1.6/cp/*.go

You need to specify the URL of a running central station server via environment variable, so the charge point can reach
it.

Docker

A containerized version of the charge point example is available:

  1. docker pull ldonini/ocpp1.6-charge-point:latest
  2. docker run -e CLIENT_ID=chargePointSim -e CENTRAL_SYSTEM_URL=ws://<host>:8887 -it --rm --name charge-point ldonini/ocpp1.6-charge-point:latest

You need to specify the host, on which the central system is running, in order for the charge point to connect to it.

You can also run it directly using docker-compose:

  1. docker-compose -f example/1.6/docker-compose.yml up charge-point

TLS

If you wish to test the charge point using TLS, make sure you put your self-signed certificates inside the
example/1.6/certs folder.

Feel free to use the utility script cd example/1.6 && ./create-test-certificates.sh for generating test certificates.

Then run the following:

  1. docker-compose -f example/1.6/docker-compose.tls.yml up charge-point

OCPP 1.6 Security extension

The library supports the OCPP 1.6 Security extension, which adds support for additional messages that aren’t a part of
original OCPP 1.6 specification. The security extension is optional, but recommended to implement.

There aren’t any clear examples how to determine if a charge point supports security extensions via SupportedProfiles
configuration key or which profiles are required to be implemented in order to support the security extension.
As of now, the security extension is split into multiple profiles/functional blocks:

  • ExtendedTriggerMessage
  • Certificates (certificate management)
  • Security (event notifications, certificate signing)
  • SecureFirmware (secure firmware update)
  • Logging

HTTP Basic Auth

The security extension specifies how to secure the communication between charge points and central systems
using HTTP Basic Auth and/or certificates. These are already provided in the websocket server/client
implementation.

Example charge point:

  1. wsClient := ws.NewClient()
  2. wsClient.SetBasicAuth("foo", "bar")
  3. cp := ocpp16.NewChargePoint(chargePointID, nil, wsClient)

Example central system:

  1. server := ws.NewServer()
  2. server.SetBasicAuthHandler(func (username string, password string) bool {
  3. // todo Handle basic auth
  4. return true
  5. })
  6. cs := ocpp16.NewCentralSystem(nil, server)

Certificate-based authentication (mTLS)

The security extension specifies how to secure the communication between charge points and central systems
using mTLS (client certificates). The library provides the necessary functionality to configure TLS,
but mTLS itself is not in scope and should be handled by the user.

Additional configuration keys

The OCPP 1.6 security extension introduces additional configuration keys.
These are not a part of the standard library, but they impact how the charge point should behave.

The charge point websocket client should be restarted when the AuthorizationKey configuration changes.

Central System

To add support for security extension in the central system, you have the following handlers:

  1. // Support callbacks for all OCPP 1.6 profiles
  2. handler := &CentralSystemHandler{chargePoints: map[string]*ChargePointState{}}
  3. centralSystem.SetCoreHandler(handler)
  4. centralSystem.SetLocalAuthListHandler(handler)
  5. centralSystem.SetFirmwareManagementHandler(handler)
  6. centralSystem.SetReservationHandler(handler)
  7. centralSystem.SetRemoteTriggerHandler(handler)
  8. centralSystem.SetSmartChargingHandler(handler)
  9. // Add callbacks for OCPP 1.6 security profiles
  10. centralSystem.SetSecurityHandler(handler)
  11. centralSystem.SetSecureFirmwareHandler(handler)
  12. centralSystem.SetLogHandler(handler)

Charge Point

To add support for security extension in the charge point, you have the following handlers:

  1. handler := &ChargePointHandler{}
  2. // Support callbacks for all OCPP 1.6 profiles
  3. chargePoint.SetCoreHandler(handler)
  4. chargePoint.SetFirmwareManagementHandler(handler)
  5. chargePoint.SetLocalAuthListHandler(handler)
  6. chargePoint.SetReservationHandler(handler)
  7. chargePoint.SetRemoteTriggerHandler(handler)
  8. chargePoint.SetSmartChargingHandler(handler)
  9. // OCPP 1.6j Security extension
  10. chargePoint.SetCertificateHandler(handler)
  11. chargePoint.SetLogHandler(handler)
  12. chargePoint.SetSecureFirmwareHandler(handler)
  13. chargePoint.SetExtendedTriggerMessageHandler(handler)
  14. chargePoint.SetSecurityHandler(handler)

Advanced Features

The library offers several advanced features, especially at websocket and ocpp-j level.

Automatic message validation

All incoming and outgoing messages are validated by default, using the validator
package.
Constraints are defined on every request/response struct, as per OCPP specs.

Validation may be disabled at a package level if needed:

  1. ocppj.SetMessageValidation(false)

Use at your own risk, as this will disable validation for all messages!

I will be evaluating the possibility to selectively disable validation for a specific message,
e.g. by passing message options.

Verbose logging

The ws and ocppj packages offer the possibility to enable verbose logs, via your logger of choice, e.g.:

  1. // Setup your own logger
  2. log = logrus.New()
  3. log.SetFormatter(&logrus.TextFormatter{FullTimestamp: true})
  4. log.SetLevel(logrus.DebugLevel) // Debug level needed to see all logs
  5. // Pass own logger to ws and ocppj packages
  6. ws.SetLogger(log.WithField("logger", "websocket"))
  7. ocppj.SetLogger(log.WithField("logger", "ocppj"))

The logger you pass needs to conform to the logging.Logger interface.
Commonly used logging libraries, such as zap or logrus, adhere to this interface out-of-the-box.

If you are using a logger, that isn’t conform, you can simply write an adapter between the Logger interface and your
own logging system.

Websocket ping-pong

The websocket package supports configuring ping pong for both endpoints.

By default, the client sends a ping every 54 seconds and waits for a pong for 60 seconds, before timing out.
The values can be configured as follows:

  1. cfg := ws.NewClientTimeoutConfig()
  2. cfg.PingPeriod = 10 * time.Second
  3. cfg.PongWait = 20 * time.Second
  4. websocketClient.SetTimeoutConfig(cfg)

By default, the server does not send out any pings and waits for a ping from the client for 60 seconds, before timing out.
To configure the server to send out pings, the PingPeriod and PongWait must be set to a value greater than 0:

  1. cfg := ws.NewServerTimeoutConfig()
  2. cfg.PingPeriod = 10 * time.Second
  3. cfg.PongWait = 20 * time.Second
  4. websocketServer.SetTimeoutConfig(cfg)

To disable sending ping messages, set the PingPeriod value to 0.

OCPP 2.0.1 Usage

Experimental support for version 2.0.1 is now supported!

Version 2.0 was skipped entirely, since it is considered obsolete.

Requests and responses in OCPP 2.0.1 are handled the same way they were in v1.6.
The notable change is that there are now significantly more supported messages and profiles (feature sets),
which also require their own handlers to be implemented.

The library API to the lower websocket and ocpp-j layers remains unchanged.

Below are very minimal setup code snippets, to get you started.
CSMS is now the equivalent of the Central System,
while the Charging Station is the new equivalent of a Charge Point.

Refer to the examples folder for a full working example.
More in-depth documentation for v2.0.1 will follow.

Bug reports for this version are welcome.

CSMS

To start a CSMS instance, run the following:

  1. import "github.com/lorenzodonini/ocpp-go/ocpp2.0.1"
  2. csms := ocpp2.NewCSMS(nil, nil)
  3. // Set callback handlers for connect/disconnect
  4. csms.SetNewChargingStationHandler(func (chargingStation ocpp2.ChargingStationConnection) {
  5. log.Printf("new charging station %v connected", chargingStation.ID())
  6. })
  7. csms.SetChargingStationDisconnectedHandler(func (chargingStation ocpp2.ChargingStationConnection) {
  8. log.Printf("charging station %v disconnected", chargingStation.ID())
  9. })
  10. // Set handler for profile callbacks
  11. handler := &CSMSHandler{}
  12. csms.SetAuthorizationHandler(handler)
  13. csms.SetAvailabilityHandler(handler)
  14. csms.SetDiagnosticsHandler(handler)
  15. csms.SetFirmwareHandler(handler)
  16. csms.SetLocalAuthListHandler(handler)
  17. csms.SetMeterHandler(handler)
  18. csms.SetProvisioningHandler(handler)
  19. csms.SetRemoteControlHandler(handler)
  20. csms.SetReservationHandler(handler)
  21. csms.SetTariffCostHandler(handler)
  22. csms.SetTransactionsHandler(handler)
  23. // Start central system
  24. listenPort := 8887
  25. log.Printf("starting CSMS")
  26. csms.Start(listenPort, "/{ws}") // This call starts server in daemon mode and is blocking
  27. log.Println("stopped CSMS")

Sending requests

Similarly to v1.6, you may send requests using the simplified API, e.g.

  1. err := csms.GetLocalListVersion(chargingStationID, myCallback)
  2. if err != nil {
  3. log.Printf("error sending message: %v", err)
  4. }

Or you may build requests manually and send them using the asynchronous API.

Docker image

There is a Dockerfile and a docker image available upstream.
Feel free

Charging Station

To start a charging station instance, simply run the following:

  1. chargingStationID := "cs0001"
  2. csmsUrl = "ws://localhost:8887"
  3. chargingStation := ocpp2.NewChargingStation(chargingStationID, nil, nil)
  4. // Set a handler for all callback functions
  5. handler := &ChargingStationHandler{}
  6. chargingStation.SetAvailabilityHandler(handler)
  7. chargingStation.SetAuthorizationHandler(handler)
  8. chargingStation.SetDataHandler(handler)
  9. chargingStation.SetDiagnosticsHandler(handler)
  10. chargingStation.SetDisplayHandler(handler)
  11. chargingStation.SetFirmwareHandler(handler)
  12. chargingStation.SetISO15118Handler(handler)
  13. chargingStation.SetLocalAuthListHandler(handler)
  14. chargingStation.SetProvisioningHandler(handler)
  15. chargingStation.SetRemoteControlHandler(handler)
  16. chargingStation.SetReservationHandler(handler)
  17. chargingStation.SetSmartChargingHandler(handler)
  18. chargingStation.SetTariffCostHandler(handler)
  19. chargingStation.SetTransactionsHandler(handler)
  20. // Connects to CSMS
  21. err := chargingStation.Start(csmsUrl)
  22. if err != nil {
  23. log.Println(err)
  24. } else {
  25. log.Printf("connected to CSMS at %v", csmsUrl)
  26. mainRoutine() // ... your program logic goes here
  27. }
  28. // Disconnect
  29. chargingStation.Stop()
  30. log.Println("disconnected from CSMS")

Sending requests

Similarly to v1.6 you may send requests using the simplified API (recommended), e.g.

  1. bootResp, err := chargingStation.BootNotification(provisioning.BootReasonPowerUp, "model1", "vendor1")
  2. if err != nil {
  3. log.Printf("error sending message: %v", err)
  4. } else {
  5. log.Printf("status: %v, interval: %v, current time: %v", bootResp.Status, bootResp.Interval, bootResp.CurrentTime.String())
  6. }

Or you may build requests manually and send them using either the synchronous or asynchronous API.

Contributing

If you’re contributing a code change, you’ll want to be sure the tests are passing first; here are the steps to check
that:

  • Install toxiproxy for your platform
  • Shell 1 - toxiproxy-server -port 8474 -host localhost
  • Shell 2 - go fmt ./... && go vet ./... && go test -v -count=1 -failfast ./...

Generating mocks

For generating mocks, the mockery tool is used. For mockery installation, follow the instructions on
the official docs.

When adding new interfaces and needing to generate mocks, you should:

  1. Add the package/interface to the .mockery.yaml file. Be mindful of the naming conventions. The mocks should mostly
    be generated in the same package as the interface, be snake case and suffixed with _mock.go. Following the existing
    examples should give you a good idea of how to proceed.

  2. Run the following command:

    1. mockery