diff --git a/.gitignore b/.gitignore index 67036729..f8947ca0 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ # Idea .idea/* + +# Project-specific +certs/ diff --git a/.travis.yml b/.travis.yml index 2d09cb56..dac6566b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,19 +17,26 @@ jobs: - stage: test script: - go build ./... - - go test ./... + - go get golang.org/x/tools/cmd/cover + - go get github.com/mattn/goveralls + - go test ./ws ./ocppj -v -covermode=count -coverprofile=coverage.out + - go test -v -covermode=count -coverprofile=ocpp16.out -coverpkg=github.com/lorenzodonini/ocpp-go/ocpp1.6/... github.com/lorenzodonini/ocpp-go/ocpp1.6_test + - go test -v -covermode=count -coverprofile=ocpp20.out -coverpkg=github.com/lorenzodonini/ocpp-go/ocpp2.0/... github.com/lorenzodonini/ocpp-go/ocpp2.0_test + - sed '1d;$d' ocpp16.out >> coverage.out + - sed '1d;$d' ocpp20.out >> coverage.out + - goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN - stage: publish_latest script: - - docker build -t ldonini/ocpp1.6-central-system:latest -f example/cs1.6/Dockerfile . - - docker build -t ldonini/ocpp1.6-charge-point:latest -f example/cp1.6/Dockerfile . + - docker build -t ldonini/ocpp1.6-central-system:latest -f example/1.6/cs/Dockerfile . + - docker build -t ldonini/ocpp1.6-charge-point:latest -f example/1.6/cp/Dockerfile . - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin - docker push ldonini/ocpp1.6-central-system:latest - docker push ldonini/ocpp1.6-charge-point:latest if: branch == master - stage: release script: - - docker build -t "ldonini/ocpp1.6-central-system:$TRAVIS_TAG" -f example/cs1.6/Dockerfile . - - docker build -t "ldonini/ocpp1.6-charge-point:$TRAVIS_TAG" -f example/cp1.6/Dockerfile . + - docker build -t "ldonini/ocpp1.6-central-system:$TRAVIS_TAG" -f example/1.6/cs/Dockerfile . + - docker build -t "ldonini/ocpp1.6-charge-point:$TRAVIS_TAG" -f example/1.6/cp/Dockerfile . - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin - docker push "ldonini/ocpp1.6-central-system:$TRAVIS_TAG" - docker push "ldonini/ocpp1.6-charge-point:$TRAVIS_TAG" diff --git a/README.md b/README.md index 9c91201d..1cc428fe 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # ocpp-go [![Build Status](https://travis-ci.org/lorenzodonini/ocpp-go.svg?branch=master)](https://travis-ci.org/lorenzodonini/ocpp-go) +[![GoDoc](https://img.shields.io/badge/godoc-reference-5272B4)](https://godoc.org/github.com/lorenzodonini/ocpp-go) +[![Coverage Status](https://coveralls.io/repos/github/lorenzodonini/ocpp-go/badge.svg?branch=ocpp1.6/examples)](https://coveralls.io/github/lorenzodonini/ocpp-go?branch=ocpp1.6/examples) +[![Go report](https://goreportcard.com/badge/github.com/lorenzodonini/ocpp-go)](https://goreportcard.com/report/github.com/lorenzodonini/ocpp-go) Open Charge Point Protocol implementation in Go. @@ -9,14 +12,14 @@ The library targets modern charge points and central systems, running OCPP versi 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. -## Roadmap +## 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: - [x] OCPP 1.6 -- [ ] OCPP 2.0 - -**Note: The library is still a WIP, therefore expect some APIs to change.** +- [ ] OCPP 2.0 ## OCPP 1.6 Usage @@ -33,24 +36,32 @@ export GO111MODULE=on go mod download ``` -Your application may either act as a Central System (server) or as a Charge Point (client). +Your application may either act as a [Central System](#central-system) (server) or as a [Charge Point](#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.: ```go +import ( + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" + "time" +) + +const defaultHeartbeatInterval = 600 + type CentralSystemHandler struct { // ... your own state variables } -func (handler * CentralSystemHandler) OnAuthorize(chargePointId string, request *ocpp16.AuthorizeRequest) (confirmation *ocpp16.AuthorizeConfirmation, err error) { +func (handler *CentralSystemHandler) OnAuthorize(chargePointId string, request *core.AuthorizeRequest) (confirmation *core.AuthorizeConfirmation, err error) { // ... your own custom logic - return ocpp16.NewAuthorizationConfirmation(ocpp16.NewIdTagInfo(ocpp16.AuthorizationStatusAccepted)), nil + return core.NewAuthorizationConfirmation(types.NewIdTagInfo(types.AuthorizationStatusAccepted)), nil } -func (handler * CentralSystemHandler) OnBootNotification(chargePointId string, request *ocpp16.BootNotificationRequest) (confirmation *ocpp16.BootNotificationConfirmation, err error) { +func (handler *CentralSystemHandler) OnBootNotification(chargePointId string, request *core.BootNotificationRequest) (confirmation *core.BootNotificationConfirmation, err error) { // ... your own custom logic - return ocpp16.NewBootNotificationConfirmation(ocpp16.NewDateTime(time.Now()), defaultHeartbeatInterval, ocpp16.RegistrationStatusAccepted), nil + return core.NewBootNotificationConfirmation(types.NewDateTime(time.Now()), defaultHeartbeatInterval, core.RegistrationStatusAccepted), nil } // further callbacks... @@ -60,7 +71,7 @@ Every time a request from the charge point comes in, the respective callback fun 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 `ocpp16.CentralSystemCoreListener` interface. +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. @@ -78,7 +89,7 @@ centralSystem.SetChargePointDisconnectedHandler(func(chargePointId string) { // Set handler for profile callbacks handler := &CentralSystemHandler{} -centralSystem.SetCentralSystemCoreListener(handler) +centralSystem.SetCoreHandler(handler) // Start central system listenPort := 8887 @@ -91,7 +102,7 @@ log.Println("stopped central system") To send requests to the charge point, you may either use the simplified API: ```go -err := centralSystem.ChangeAvailability("1234", myCallback, 1, ocpp16.AvailabilityTypeInoperative) +err := centralSystem.ChangeAvailability("1234", myCallback, 1, core.AvailabilityTypeInoperative) if err != nil { log.Printf("error sending message: %v", err) } @@ -99,7 +110,7 @@ if err != nil { or create a message manually: ```go -request := ocpp16.NewChangeAvailabilityRequest(1, ocpp16.AvailabilityTypeInoperative) +request := core.NewChangeAvailabilityRequest(1, core.AvailabilityTypeInoperative) err := centralSystem.SendRequestAsync("clientId", request, callbackFunction) if err != nil { log.Printf("error sending message: %v", err) @@ -109,7 +120,7 @@ if err != nil { 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: ```go -myCallback := func(confirmation *ocpp16.ChangeAvailabilityConfirmation, e error) { +myCallback := func(confirmation *core.ChangeAvailabilityConfirmation, e error) { if e != nil { log.Printf("operation failed: %v", e) } else { @@ -119,14 +130,14 @@ myCallback := func(confirmation *ocpp16.ChangeAvailabilityConfirmation, e error) } ``` -Since the initial `centralSystem.Start` call blocks forever, you may want to wrap it in a goroutine (that is, if you need to send requests to charge points form the main thread). +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 inside `central_system_sim.go`. +You can take a look at the [full example](./example/1.6/cs/central_system_sim.go). To run it, simply execute: ```bash -go run ./example/cs/central_system_sim.go +go run ./example/1.6/cs/*.go ``` #### Docker @@ -137,36 +148,52 @@ docker pull ldonini/ocpp1.6-central-system:latest docker run -it -p 8887:8887 --rm --name central-system ldonini/ocpp1.6-central-system:latest ``` -You can also build the docker image from source, using: +You can also run it directly using docker-compose: ```sh -docker-compose up central_system +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: +``` +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.: ```go +import ( + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" +) + type ChargePointHandler struct { // ... your own state variables } -func (handler * ChargePointHandler) OnChangeAvailability(request *ocpp16.ChangeAvailabilityRequest) (confirmation *ocpp16.ChangeAvailabilityConfirmation, err error) { +func (handler *ChargePointHandler) OnChangeAvailability(request *core.ChangeAvailabilityRequest) (confirmation *core.ChangeAvailabilityConfirmation, err error) { // ... your own custom logic - return ocpp16.NewChangeAvailabilityConfirmation(ocpp16.AvailabilityStatusAccepted), nil + return core.NewChangeAvailabilityConfirmation(core.AvailabilityStatusAccepted), nil } -func (handler * ChargePointHandler) OnChangeConfiguration(request *ocpp16.ChangeConfigurationRequest) (confirmation *ocpp16.ChangeConfigurationConfirmation, err error) { +func (handler *ChargePointHandler) OnChangeConfiguration(request *core.ChangeConfigurationRequest) (confirmation *core.ChangeConfigurationConfirmation, err error) { // ... your own custom logic - return ocpp16.NewChangeConfigurationConfirmation(ocpp16.ConfigurationStatusAccepted), nil + return core.NewChangeConfigurationConfirmation(core.ConfigurationStatusAccepted), nil } // further callbacks... ``` -When a request from the central system comes in, the respective callback function is called. +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 `ocpp16.ChargePointCoreListener` interface. +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. @@ -178,7 +205,7 @@ chargePoint := ocpp16.NewChargePoint(chargePointId, nil, nil) // Set a handler for all callback functions handler := &ChargePointHandler{} -chargePoint.SetChargePointCoreListener(handler) +chargePoint.SetCoreHandler(handler) // Connects to central system err := chargePoint.Start(csUrl) @@ -208,7 +235,7 @@ log.Printf("status: %v, interval: %v, current time: %v", bootConf.Status, bootCo or create a message manually: ```go -request := ocpp16.NewBootNotificationRequest("model1", "vendor1") +request := core.NewBootNotificationRequest("model1", "vendor1") ``` You can then decide to send the message using a synchronous blocking call: @@ -218,7 +245,7 @@ confirmation, err := chargePoint.SendRequest(request) if err != nil { log.Printf("error sending message: %v", err) } -bootConf := confirmation.(*ocpp16.BootNotificationConfirmation) +bootConf := confirmation.(*core.BootNotificationConfirmation) // ... do something with the confirmation ``` or an asynchronous call: @@ -232,8 +259,8 @@ if err != nil { In the latter case, you need to write the callback function and check for errors on your own: ```go -callback := func(confirmation ocpp.Confirmation, e error) { - bootConf := confirmation.(*ocpp16.BootNotificationConfirmation) +callback := func(confirmation ocpp.Response, e error) { + bootConf := confirmation.(*core.BootNotificationConfirmation) if e != nil { log.Printf("operation failed: %v", e) } else { @@ -246,13 +273,13 @@ callback := func(confirmation ocpp.Confirmation, e error) { 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 inside `charge_point_sim.go`. +You can take a look at the [full example](./example/1.6/cp/charge_point_sim.go). To run it, simply execute: ```bash -CLIENT_ID=chargePointSim CENTRAL_SYSTEM_URL=ws://:8887 go run ./example/cp/charge_point_sim.go +CLIENT_ID=chargePointSim CENTRAL_SYSTEM_URL=ws://:8887 go run example/1.6/cp/*.go ``` -You need to specify the hostname/IP of a running central station server, so the charge point can reach it. +You need to specify the URL of a running central station server via environment variable, so the charge point can reach it. #### Docker @@ -264,7 +291,22 @@ docker run -e CLIENT_ID=chargePointSim -e CENTRAL_SYSTEM_URL=ws://:8887 -i 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 build the docker image from source, using: +You can also run it directly using docker-compose: ```sh -docker-compose up charge_point +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: +``` +docker-compose -f example/1.6/docker-compose.tls.yml up charge_point +``` + +## OCPP 2.0 Usage + +Documentation will follow, once the protocol is fully implemented. diff --git a/example/cp1.6/Dockerfile b/example/1.6/cp/Dockerfile similarity index 53% rename from example/cp1.6/Dockerfile rename to example/1.6/cp/Dockerfile index 78e9c38c..642cd7d5 100644 --- a/example/cp1.6/Dockerfile +++ b/example/1.6/cp/Dockerfile @@ -9,13 +9,18 @@ COPY . . # Fetch dependencies. RUN go mod download # Build the binary. -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/charge_point example/cp1.6/charge_point_sim.go +RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/charge_point example/1.6/cp/*.go ############################ # STEP 2 build a small image ############################ -FROM scratch +FROM alpine COPY --from=builder /go/bin/charge_point /bin/charge_point +# Add CA certificates +# It currently throws a warning on alpine: WARNING: ca-certificates.crt does not contain exactly one certificate or CRL: skipping. +# Ignore the warning. +RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/* && update-ca-certificates + CMD [ "charge_point" ] diff --git a/example/1.6/cp/charge_point_sim.go b/example/1.6/cp/charge_point_sim.go new file mode 100644 index 00000000..4122972f --- /dev/null +++ b/example/1.6/cp/charge_point_sim.go @@ -0,0 +1,213 @@ +package main + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + ocpp16 "github.com/lorenzodonini/ocpp-go/ocpp1.6" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/localauth" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" + "github.com/lorenzodonini/ocpp-go/ws" + log "github.com/sirupsen/logrus" + "io/ioutil" + "os" + "strconv" + "time" +) + +const ( + envVarClientID = "CLIENT_ID" + envVarCentralSystemUrl = "CENTRAL_SYSTEM_URL" + envVarTls = "TLS_ENABLED" + envVarCACertificate = "CA_CERTIFICATE_PATH" + envVarClientCertificate = "CLIENT_CERTIFICATE_PATH" + envVarClientCertificateKey = "CLIENT_CERTIFICATE_KEY_PATH" +) + +func setupChargePoint(chargePointID string) ocpp16.ChargePoint { + return ocpp16.NewChargePoint(chargePointID, nil, nil) +} + +func setupTlsChargePoint(chargePointID string) ocpp16.ChargePoint { + certPool, err := x509.SystemCertPool() + if err != nil { + log.Fatal(err) + } + // Load CA cert + caPath, ok := os.LookupEnv(envVarCACertificate) + if !ok { + log.Fatalf("no required %v found", envVarCACertificate) + } + caCert, err := ioutil.ReadFile(caPath) + if err != nil { + log.Warn(err) + } else if !certPool.AppendCertsFromPEM(caCert) { + log.Warn("no ca.cert file found, will use system CA certificates") + } + // Load client certificate + clientCertPath, ok1 := os.LookupEnv(envVarClientCertificate) + clientKeyPath, ok2 := os.LookupEnv(envVarClientCertificateKey) + var clientCertificates []tls.Certificate + if ok1 && ok2 { + certificate, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath) + if err == nil { + clientCertificates = []tls.Certificate{certificate} + } else { + log.Infof("couldn't load client TLS certificate: %v", err) + } + } + // Create client with TLS config + client := ws.NewTLSClient(&tls.Config{ + RootCAs: certPool, + Certificates: clientCertificates, + }) + return ocpp16.NewChargePoint(chargePointID, nil, client) +} + +// Used for scheduling requests that are not generated from the main thread. +// OCPP is a request-response protocol and doesn't support multiple simultaneous requests. +// +// By scheduling a request, it can later on be sent safely from the main thread. +func scheduleAsyncRequest(asyncRequest func()) { + asyncRequestChan <- asyncRequest +} + +// Wait is used for simulating idle time, while being able to process trigger requests. +// +// If trigger requests (or other asynchronous requests) are fired while waiting, the request will be sent. +// This mechanism prevents errors when attempting to send a request while another one is still pending. +func wait(d time.Duration) { + t := time.NewTimer(d) + for { + select { + case req, ok := <-asyncRequestChan: + if !ok { + return + } + req() + case <-t.C: + log.Debugf("finished simulated wait") + return + } + } +} + +// exampleRoutine simulates a charge point flow, where +func exampleRoutine(chargePoint ocpp16.ChargePoint, stateHandler *ChargePointHandler) { + dummyClientIdTag := "12345" + chargingConnector := 1 + // Boot + bootConf, err := chargePoint.BootNotification("model1", "vendor1") + checkError(err) + logDefault(bootConf.GetFeatureName()).Infof("status: %v, interval: %v, current time: %v", bootConf.Status, bootConf.Interval, bootConf.CurrentTime.String()) + // Notify connector status + updateStatus(stateHandler, 0, core.ChargePointStatusAvailable) + // Wait for some time ... + wait(5 * time.Second) + // Simulate charging for connector 1 + authConf, err := chargePoint.Authorize(dummyClientIdTag) + checkError(err) + logDefault(authConf.GetFeatureName()).Infof("status: %v %v", authConf.IdTagInfo.Status, getExpiryDate(authConf.IdTagInfo)) + // Update connector status + updateStatus(stateHandler, chargingConnector, core.ChargePointStatusPreparing) + // Start transaction + startConf, err := chargePoint.StartTransaction(chargingConnector, dummyClientIdTag, stateHandler.meterValue, types.NewDateTime(time.Now())) + checkError(err) + logDefault(startConf.GetFeatureName()).Infof("status: %v, transaction %v %v", startConf.IdTagInfo.Status, startConf.TransactionId, getExpiryDate(startConf.IdTagInfo)) + stateHandler.connectors[chargingConnector].currentTransaction = startConf.TransactionId + // Update connector status + updateStatus(stateHandler, chargingConnector, core.ChargePointStatusCharging) + // Periodically send meter values + for i := 0; i < 5; i++ { + sampleInterval, ok := stateHandler.configuration.getInt(MeterValueSampleInterval) + if !ok { + sampleInterval = 5 + } + wait(time.Second * time.Duration(sampleInterval)) + stateHandler.meterValue += 10 + sampledValue := types.SampledValue{Value: fmt.Sprintf("%v", stateHandler.meterValue), Unit: types.UnitOfMeasureWh, Format: types.ValueFormatRaw, Measurand: types.MeasurandEnergyActiveExportRegister, Context: types.ReadingContextSamplePeriodic, Location: types.LocationOutlet} + meterValue := types.MeterValue{Timestamp: types.NewDateTime(time.Now()), SampledValue: []types.SampledValue{sampledValue}} + meterConf, err := chargePoint.MeterValues(chargingConnector, []types.MeterValue{meterValue}) + checkError(err) + logDefault(meterConf.GetFeatureName()).Infof("sent updated %v", sampledValue.Measurand) + } + stateHandler.meterValue += 2 + // Stop charging for connector 1 + updateStatus(stateHandler, chargingConnector, core.ChargePointStatusFinishing) + stopConf, err := chargePoint.StopTransaction(stateHandler.meterValue, types.NewDateTime(time.Now()), startConf.TransactionId, func(request *core.StopTransactionRequest) { + sampledValue := types.SampledValue{Value: fmt.Sprintf("%v", stateHandler.meterValue), Unit: types.UnitOfMeasureWh, Format: types.ValueFormatRaw, Measurand: types.MeasurandEnergyActiveExportRegister, Context: types.ReadingContextSamplePeriodic, Location: types.LocationOutlet} + meterValue := types.MeterValue{Timestamp: types.NewDateTime(time.Now()), SampledValue: []types.SampledValue{sampledValue}} + request.TransactionData = []types.MeterValue{meterValue} + request.Reason = core.ReasonEVDisconnected + }) + checkError(err) + logDefault(stopConf.GetFeatureName()).Infof("transaction %v stopped", startConf.TransactionId) + // Update connector status + updateStatus(stateHandler, chargingConnector, core.ChargePointStatusAvailable) +} + +// Start function +func main() { + // Load config + id, ok := os.LookupEnv(envVarClientID) + if !ok { + log.Printf("no %v environment variable found, exiting...", envVarClientID) + return + } + csUrl, ok := os.LookupEnv(envVarCentralSystemUrl) + if !ok { + log.Printf("no %v environment variable found, exiting...", envVarCentralSystemUrl) + return + } + // Check if TLS enabled + t, _ := os.LookupEnv(envVarTls) + tlsEnabled, _ := strconv.ParseBool(t) + // Prepare OCPP 1.6 charge point (chargePoint variable is defined in handler.go) + if tlsEnabled { + chargePoint = setupTlsChargePoint(id) + } else { + chargePoint = setupChargePoint(id) + } + // Setup some basic state management + connectors := map[int]*ConnectorInfo{ + 1: {status: core.ChargePointStatusAvailable, availability: core.AvailabilityTypeOperative, currentTransaction: 0}, + } + handler := &ChargePointHandler{ + status: core.ChargePointStatusAvailable, + connectors: connectors, + configuration: getDefaultConfig(), + errorCode: core.NoError, + localAuthList: []localauth.AuthorizationData{}, + localAuthListVersion: 0} + // Support callbacks for all OCPP 1.6 profiles + chargePoint.SetCoreHandler(handler) + chargePoint.SetFirmwareManagementHandler(handler) + chargePoint.SetLocalAuthListHandler(handler) + chargePoint.SetReservationHandler(handler) + chargePoint.SetRemoteTriggerHandler(handler) + chargePoint.SetSmartChargingHandler(handler) + // Connects to central system + err := chargePoint.Start(csUrl) + if err != nil { + log.Println(err) + } else { + log.Infof("connected to central system at %v", csUrl) + // Setup channel for asynchronous requests (e.g. triggers) + asyncRequestChan = make(chan func(), 5) + exampleRoutine(chargePoint, handler) + close(asyncRequestChan) + // Disconnect + chargePoint.Stop() + log.Infof("disconnected from central system") + } +} + +func init() { + log.SetLevel(log.InfoLevel) +} + +// Utility functions +func logDefault(feature string) *log.Entry { + return log.WithField("message", feature) +} diff --git a/example/1.6/cp/config.go b/example/1.6/cp/config.go new file mode 100644 index 00000000..47ff5bc8 --- /dev/null +++ b/example/1.6/cp/config.go @@ -0,0 +1,122 @@ +package main + +import ( + "fmt" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/firmware" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/localauth" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/remotetrigger" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/reservation" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/smartcharging" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" + "strconv" +) + +const ( + AuthorizeRemoteTxRequests string = "AuthorizeRemoteTxRequests" + ClockAlignedDataInterval string = "ClockAlignedDataInterval" + ConnectionTimeOut string = "ConnectionTimeOut" + ConnectorPhaseRotation string = "ConnectorPhaseRotation" + GetConfigurationMaxKeys string = "GetConfigurationMaxKeys" + HeartbeatInterval string = "HeartbeatInterval" + LocalAuthorizeOffline string = "LocalAuthorizeOffline" + LocalPreAuthorize string = "LocalPreAuthorize" + MeterValuesAlignedData string = "MeterValuesAlignedData" + MeterValuesSampledData string = "MeterValuesSampledData" + MeterValueSampleInterval string = "MeterValueSampleInterval" + NumberOfConnectors string = "NumberOfConnectors" + ResetRetries string = "ResetRetries" + StopTransactionOnEVSideDisconnect string = "StopTransactionOnEVSideDisconnect" + StopTransactionOnInvalidId string = "StopTransactionOnInvalidId" + StopTxnAlignedData string = "StopTxnAlignedData" + StopTxnSampledData string = "StopTxnSampledData" + SupportedFeatureProfiles string = "SupportedFeatureProfiles" + TransactionMessageAttempts string = "TransactionMessageAttempts" + TransactionMessageRetryInterval string = "TransactionMessageRetryInterval" + UnlockConnectorOnEVSideDisconnect string = "UnlockConnectorOnEVSideDisconnect" + WebSocketPingInterval string = "WebSocketPingInterval" + LocalAuthListEnabled string = "LocalAuthListEnabled" + LocalAuthListMaxLength string = "LocalAuthListMaxLength" + SendLocalListMaxLength string = "SendLocalListMaxLength" + ChargeProfileMaxStackLevel string = "ChargeProfileMaxStackLevel" + ChargingScheduleAllowedChargingRateUnit string = "ChargingScheduleAllowedChargingRateUnit" + ChargingScheduleMaxPeriods string = "ChargingScheduleMaxPeriods" + MaxChargingProfilesInstalled string = "MaxChargingProfilesInstalled" +) + +type ConfigMap map[string]core.ConfigurationKey + +func (c ConfigMap) updateInt(key string, i int64) { + configKey, ok := c[key] + if ok { + configKey.Value = strconv.FormatInt(i, 10) + } + c[key] = configKey +} + +func (c ConfigMap) updateBool(key string, b bool) { + configKey, ok := c[key] + if ok { + configKey.Value = strconv.FormatBool(b) + } + c[key] = configKey +} + +func (c ConfigMap) getInt(key string) (int, bool) { + configKey, ok := c[key] + if !ok { + return 0, ok + } + result, err := strconv.ParseInt(configKey.Value, 10, 32) + if err != nil { + return 0, false + } + return int(result), true +} + +func (c ConfigMap) getBool(key string) (bool, bool) { + configKey, ok := c[key] + if !ok { + return false, ok + } + result, err := strconv.ParseBool(configKey.Value) + if err != nil { + return false, false + } + return result, true +} + +func getDefaultConfig() ConfigMap { + intBase := 10 + cfg := map[string]core.ConfigurationKey{} + cfg[AuthorizeRemoteTxRequests] = core.ConfigurationKey{Key: AuthorizeRemoteTxRequests, Readonly: true, Value: strconv.FormatBool(false)} + cfg[ClockAlignedDataInterval] = core.ConfigurationKey{Key: ClockAlignedDataInterval, Readonly: false, Value: strconv.FormatInt(0, intBase)} + cfg[ConnectionTimeOut] = core.ConfigurationKey{Key: ConnectionTimeOut, Readonly: false, Value: strconv.FormatInt(60, intBase)} + cfg[ConnectorPhaseRotation] = core.ConfigurationKey{Key: ConnectorPhaseRotation, Readonly: false, Value: "Unknown"} + cfg[GetConfigurationMaxKeys] = core.ConfigurationKey{Key: GetConfigurationMaxKeys, Readonly: true, Value: strconv.FormatInt(50, intBase)} + cfg[HeartbeatInterval] = core.ConfigurationKey{Key: HeartbeatInterval, Readonly: false, Value: strconv.FormatInt(86400, intBase)} + cfg[LocalAuthorizeOffline] = core.ConfigurationKey{Key: LocalAuthorizeOffline, Readonly: false, Value: strconv.FormatBool(true)} + cfg[LocalPreAuthorize] = core.ConfigurationKey{Key: LocalPreAuthorize, Readonly: false, Value: strconv.FormatBool(false)} + cfg[MeterValuesAlignedData] = core.ConfigurationKey{Key: MeterValuesAlignedData, Readonly: false, Value: string(types.MeasurandEnergyActiveExportRegister)} + cfg[MeterValuesSampledData] = core.ConfigurationKey{Key: MeterValuesSampledData, Readonly: false, Value: string(types.MeasurandEnergyActiveExportRegister)} + cfg[MeterValueSampleInterval] = core.ConfigurationKey{Key: MeterValueSampleInterval, Readonly: false, Value: strconv.FormatInt(5, intBase)} + cfg[NumberOfConnectors] = core.ConfigurationKey{Key: NumberOfConnectors, Readonly: true, Value: strconv.FormatInt(1, intBase)} + cfg[ResetRetries] = core.ConfigurationKey{Key: ResetRetries, Readonly: false, Value: strconv.FormatInt(10, intBase)} + cfg[StopTransactionOnEVSideDisconnect] = core.ConfigurationKey{Key: StopTransactionOnEVSideDisconnect, Readonly: false, Value: strconv.FormatBool(true)} + cfg[StopTransactionOnInvalidId] = core.ConfigurationKey{Key: StopTransactionOnInvalidId, Readonly: false, Value: strconv.FormatBool(true)} + cfg[StopTxnAlignedData] = core.ConfigurationKey{Key: StopTxnAlignedData, Readonly: false, Value: strconv.FormatBool(true)} + cfg[StopTxnSampledData] = core.ConfigurationKey{Key: StopTxnSampledData, Readonly: false, Value: string(types.MeasurandEnergyActiveExportRegister)} + cfg[SupportedFeatureProfiles] = core.ConfigurationKey{Key: SupportedFeatureProfiles, Readonly: true, Value: fmt.Sprintf("%v,%v,%v,%v,%v,%v", core.ProfileName, firmware.ProfileName, localauth.ProfileName, reservation.ProfileName, remotetrigger.ProfileName, smartcharging.ProfileName)} + cfg[TransactionMessageAttempts] = core.ConfigurationKey{Key: TransactionMessageAttempts, Readonly: false, Value: strconv.FormatInt(5, intBase)} + cfg[TransactionMessageRetryInterval] = core.ConfigurationKey{Key: TransactionMessageRetryInterval, Readonly: false, Value: strconv.FormatInt(60, intBase)} + cfg[UnlockConnectorOnEVSideDisconnect] = core.ConfigurationKey{Key: UnlockConnectorOnEVSideDisconnect, Readonly: false, Value: strconv.FormatBool(true)} + cfg[WebSocketPingInterval] = core.ConfigurationKey{Key: WebSocketPingInterval, Readonly: false, Value: strconv.FormatInt(54, intBase)} + cfg[LocalAuthListEnabled] = core.ConfigurationKey{Key: LocalAuthListEnabled, Readonly: false, Value: strconv.FormatBool(true)} + cfg[LocalAuthListMaxLength] = core.ConfigurationKey{Key: LocalAuthListMaxLength, Readonly: true, Value: strconv.FormatInt(100, intBase)} + cfg[SendLocalListMaxLength] = core.ConfigurationKey{Key: SendLocalListMaxLength, Readonly: true, Value: strconv.FormatInt(20, intBase)} + cfg[ChargeProfileMaxStackLevel] = core.ConfigurationKey{Key: ChargeProfileMaxStackLevel, Readonly: true, Value: strconv.FormatInt(10, intBase)} + cfg[ChargingScheduleAllowedChargingRateUnit] = core.ConfigurationKey{Key: ChargingScheduleAllowedChargingRateUnit, Readonly: true, Value: "Power"} + cfg[ChargingScheduleMaxPeriods] = core.ConfigurationKey{Key: ChargingScheduleMaxPeriods, Readonly: true, Value: strconv.FormatInt(5, intBase)} + cfg[MaxChargingProfilesInstalled] = core.ConfigurationKey{Key: MaxChargingProfilesInstalled, Readonly: true, Value: strconv.FormatInt(10, intBase)} + return cfg +} diff --git a/example/cp1.6/charge_point_sim.go b/example/1.6/cp/handler.go similarity index 67% rename from example/cp1.6/charge_point_sim.go rename to example/1.6/cp/handler.go index 8cba4664..cd3cb08c 100644 --- a/example/cp1.6/charge_point_sim.go +++ b/example/1.6/cp/handler.go @@ -11,10 +11,9 @@ import ( "github.com/lorenzodonini/ocpp-go/ocpp1.6/smartcharging" "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" log "github.com/sirupsen/logrus" - "os" - "time" ) +// ConnectorInfo contains some simple state about a single connector. type ConnectorInfo struct { status core.ChargePointStatus availability core.AvailabilityType @@ -22,19 +21,29 @@ type ConnectorInfo struct { currentReservation int } +// ChargePointHandler contains some simple state that a charge point needs to keep. +// In production this will typically be replaced by database/API calls. type ChargePointHandler struct { status core.ChargePointStatus connectors map[int]*ConnectorInfo errorCode core.ChargePointErrorCode - configuration map[string]core.ConfigurationKey + configuration ConfigMap meterValue int localAuthList []localauth.AuthorizationData localAuthListVersion int } +var asyncRequestChan chan func() + var chargePoint ocpp16.ChargePoint -// Core profile callbacks +func (handler *ChargePointHandler) isValidConnectorID(ID int) bool { + _, ok := handler.connectors[ID] + return ok || ID == 0 +} + +// ------------- Core profile callbacks ------------- + func (handler *ChargePointHandler) OnChangeAvailability(request *core.ChangeAvailabilityRequest) (confirmation *core.ChangeAvailabilityConfirmation, err error) { handler.connectors[request.ConnectorId].availability = request.Type return core.NewChangeAvailabilityConfirmation(core.AvailabilityStatusAccepted), nil @@ -43,12 +52,15 @@ func (handler *ChargePointHandler) OnChangeAvailability(request *core.ChangeAvai func (handler *ChargePointHandler) OnChangeConfiguration(request *core.ChangeConfigurationRequest) (confirmation *core.ChangeConfigurationConfirmation, err error) { configKey, ok := handler.configuration[request.Key] if !ok { + logDefault(request.GetFeatureName()).Infof("couldn't change configuration for unsupported parameter %v", configKey.Key) return core.NewChangeConfigurationConfirmation(core.ConfigurationStatusNotSupported), nil } else if configKey.Readonly { + logDefault(request.GetFeatureName()).Infof("couldn't change configuration for readonly parameter %v", configKey.Key) return core.NewChangeConfigurationConfirmation(core.ConfigurationStatusRejected), nil } configKey.Value = request.Value handler.configuration[request.Key] = configKey + logDefault(request.GetFeatureName()).Infof("changed configuration for parameter %v to %v", configKey.Key, configKey.Value) return core.NewChangeConfigurationConfirmation(core.ConfigurationStatusAccepted), nil } @@ -115,8 +127,10 @@ func (handler *ChargePointHandler) OnUnlockConnector(request *core.UnlockConnect return core.NewUnlockConnectorConfirmation(core.UnlockStatusUnlocked), nil } -// Local authorization list profile callbacks +// ------------- Local authorization list profile callbacks ------------- + func (handler *ChargePointHandler) OnGetLocalListVersion(request *localauth.GetLocalListVersionRequest) (confirmation *localauth.GetLocalListVersionConfirmation, err error) { + logDefault(request.GetFeatureName()).Infof("returning current local list version: %v", handler.localAuthListVersion) return localauth.NewGetLocalListVersionConfirmation(handler.localAuthListVersion), nil } @@ -134,7 +148,8 @@ func (handler *ChargePointHandler) OnSendLocalList(request *localauth.SendLocalL return localauth.NewSendLocalListConfirmation(localauth.UpdateStatusAccepted), nil } -// Firmware management profile callbacks +// ------------- Firmware management profile callbacks ------------- + func (handler *ChargePointHandler) OnGetDiagnostics(request *firmware.GetDiagnosticsRequest) (confirmation *firmware.GetDiagnosticsConfirmation, err error) { return firmware.NewGetDiagnosticsConfirmation(), nil //TODO: perform diagnostics upload out-of-band @@ -145,34 +160,66 @@ func (handler *ChargePointHandler) OnUpdateFirmware(request *firmware.UpdateFirm //TODO: download new firmware out-of-band } -// Remote trigger profile callbacks +// ------------- Remote trigger profile callbacks ------------- + func (handler *ChargePointHandler) OnTriggerMessage(request *remotetrigger.TriggerMessageRequest) (confirmation *remotetrigger.TriggerMessageConfirmation, err error) { + logDefault(request.GetFeatureName()).Infof("received trigger for %v", request.RequestedMessage) + status := remotetrigger.TriggerMessageStatusRejected switch request.RequestedMessage { case core.BootNotificationFeatureName: //TODO: schedule boot notification message break case firmware.DiagnosticsStatusNotificationFeatureName: - //TODO: schedule diagnostics status notification message - break + // Schedule diagnostics status notification request + fn := func() { + _, e := chargePoint.DiagnosticsStatusNotification(firmware.DiagnosticsStatusIdle) + checkError(e) + logDefault(firmware.DiagnosticsStatusNotificationFeatureName).Info("diagnostics status notified") + } + scheduleAsyncRequest(fn) + status = remotetrigger.TriggerMessageStatusAccepted case firmware.FirmwareStatusNotificationFeatureName: //TODO: schedule firmware status notification message break case core.HeartbeatFeatureName: - //TODO: schedule heartbeat message - break + // Schedule heartbeat request + fn := func() { + conf, e := chargePoint.Heartbeat() + checkError(e) + logDefault(core.HeartbeatFeatureName).Infof("clock synchronized: %v", conf.CurrentTime.FormatTimestamp()) + } + scheduleAsyncRequest(fn) + status = remotetrigger.TriggerMessageStatusAccepted case core.MeterValuesFeatureName: //TODO: schedule meter values message break - //TODO: schedule status notification message case core.StatusNotificationFeatureName: - break + connectorID := request.ConnectorId + // Check if requested connector is valid and status can be retrieved + if !handler.isValidConnectorID(connectorID) { + logDefault(request.GetFeatureName()).Errorf("cannot trigger %v: requested invalid connector %v", request.RequestedMessage, request.ConnectorId) + return remotetrigger.NewTriggerMessageConfirmation(remotetrigger.TriggerMessageStatusRejected), nil + } + // Schedule status notification request + fn := func() { + status := handler.status + if c, ok := handler.connectors[request.ConnectorId]; ok { + status = c.status + } + statusConfirmation, err := chargePoint.StatusNotification(connectorID, handler.errorCode, status) + checkError(err) + logDefault(statusConfirmation.GetFeatureName()).Infof("status for connector %v sent: %v", connectorID, status) + } + scheduleAsyncRequest(fn) + status = remotetrigger.TriggerMessageStatusAccepted default: return remotetrigger.NewTriggerMessageConfirmation(remotetrigger.TriggerMessageStatusNotImplemented), nil } - return remotetrigger.NewTriggerMessageConfirmation(remotetrigger.TriggerMessageStatusAccepted), nil + return remotetrigger.NewTriggerMessageConfirmation(status), nil } -// Reservation profile callbacks +// ------------- Reservation profile callbacks ------------- + func (handler *ChargePointHandler) OnReserveNow(request *reservation.ReserveNowRequest) (confirmation *reservation.ReserveNowConfirmation, err error) { connector := handler.connectors[request.ConnectorId] if connector == nil { @@ -181,6 +228,7 @@ func (handler *ChargePointHandler) OnReserveNow(request *reservation.ReserveNowR return reservation.NewReserveNowConfirmation(reservation.ReservationStatusOccupied), nil } connector.currentReservation = request.ReservationId + logDefault(request.GetFeatureName()).Infof("reservation %v for connector %v accepted", request.ReservationId, request.ConnectorId) go updateStatus(handler, request.ConnectorId, core.ChargePointStatusReserved) // TODO: automatically remove reservation after expiryDate return reservation.NewReserveNowConfirmation(reservation.ReservationStatusAccepted), nil @@ -193,13 +241,16 @@ func (handler *ChargePointHandler) OnCancelReservation(request *reservation.Canc if v.status == core.ChargePointStatusReserved { go updateStatus(handler, k, core.ChargePointStatusAvailable) } + logDefault(request.GetFeatureName()).Infof("reservation %v for connector %v canceled", request.ReservationId, k) return reservation.NewCancelReservationConfirmation(reservation.CancelReservationStatusAccepted), nil } } + logDefault(request.GetFeatureName()).Infof("couldn't cancel reservation %v: reservation not found!", request.ReservationId) return reservation.NewCancelReservationConfirmation(reservation.CancelReservationStatusRejected), nil } -// Smart charging profile callbacks +// ------------- Smart charging profile callbacks ------------- + func (handler *ChargePointHandler) OnSetChargingProfile(request *smartcharging.SetChargingProfileRequest) (confirmation *smartcharging.SetChargingProfileConfirmation, err error) { //TODO: handle logic return smartcharging.NewSetChargingProfileConfirmation(smartcharging.ChargingProfileStatusNotImplemented), nil @@ -234,7 +285,7 @@ func updateStatus(stateHandler *ChargePointHandler, connector int, status core.C } else { stateHandler.connectors[connector].status = status } - statusConfirmation, err := chargePoint.StatusNotification(connector, core.NoError, status) + statusConfirmation, err := chargePoint.StatusNotification(connector, stateHandler.errorCode, status) checkError(err) if connector == 0 { logDefault(statusConfirmation.GetFeatureName()).Infof("status for all connectors updated to %v", status) @@ -242,101 +293,3 @@ func updateStatus(stateHandler *ChargePointHandler, connector int, status core.C logDefault(statusConfirmation.GetFeatureName()).Infof("status for connector %v updated to %v", connector, status) } } - -func exampleRoutine(chargePoint ocpp16.ChargePoint, stateHandler *ChargePointHandler) { - dummyClientIdTag := "12345" - chargingConnector := 1 - // Boot - bootConf, err := chargePoint.BootNotification("model1", "vendor1") - checkError(err) - logDefault(bootConf.GetFeatureName()).Infof("status: %v, interval: %v, current time: %v", bootConf.Status, bootConf.Interval, bootConf.CurrentTime.String()) - // Notify connector status - updateStatus(stateHandler, 0, core.ChargePointStatusAvailable) - // Wait for some time ... - time.Sleep(5 * time.Second) - // Simulate charging for connector 1 - authConf, err := chargePoint.Authorize(dummyClientIdTag) - checkError(err) - logDefault(authConf.GetFeatureName()).Infof("status: %v %v", authConf.IdTagInfo.Status, getExpiryDate(authConf.IdTagInfo)) - // Update connector status - updateStatus(stateHandler, chargingConnector, core.ChargePointStatusPreparing) - // Start transaction - startConf, err := chargePoint.StartTransaction(chargingConnector, dummyClientIdTag, stateHandler.meterValue, types.NewDateTime(time.Now())) - checkError(err) - logDefault(startConf.GetFeatureName()).Infof("status: %v, transaction %v %v", startConf.IdTagInfo.Status, startConf.TransactionId, getExpiryDate(startConf.IdTagInfo)) - stateHandler.connectors[chargingConnector].currentTransaction = startConf.TransactionId - // Update connector status - updateStatus(stateHandler, chargingConnector, core.ChargePointStatusCharging) - // Periodically send meter values - for i := 0; i < 5; i++ { - time.Sleep(5 * time.Second) - stateHandler.meterValue += 10 - sampledValue := types.SampledValue{Value: fmt.Sprintf("%v", stateHandler.meterValue), Unit: types.UnitOfMeasureWh, Format: types.ValueFormatRaw, Measurand: types.MeasurandEnergyActiveExportRegister, Context: types.ReadingContextSamplePeriodic, Location: types.LocationOutlet} - meterValue := types.MeterValue{Timestamp: types.NewDateTime(time.Now()), SampledValue: []types.SampledValue{sampledValue}} - meterConf, err := chargePoint.MeterValues(chargingConnector, []types.MeterValue{meterValue}) - checkError(err) - logDefault(meterConf.GetFeatureName()).Infof("sent updated %v", sampledValue.Measurand) - } - stateHandler.meterValue += 2 - // Stop charging for connector 1 - updateStatus(stateHandler, chargingConnector, core.ChargePointStatusFinishing) - stopConf, err := chargePoint.StopTransaction(stateHandler.meterValue, types.NewDateTime(time.Now()), startConf.TransactionId, func(request *core.StopTransactionRequest) { - sampledValue := types.SampledValue{Value: fmt.Sprintf("%v", stateHandler.meterValue), Unit: types.UnitOfMeasureWh, Format: types.ValueFormatRaw, Measurand: types.MeasurandEnergyActiveExportRegister, Context: types.ReadingContextSamplePeriodic, Location: types.LocationOutlet} - meterValue := types.MeterValue{Timestamp: types.NewDateTime(time.Now()), SampledValue: []types.SampledValue{sampledValue}} - request.TransactionData = []types.MeterValue{meterValue} - request.Reason = core.ReasonEVDisconnected - }) - checkError(err) - logDefault(stopConf.GetFeatureName()).Infof("transaction %v stopped", startConf.TransactionId) - // Update connector status - updateStatus(stateHandler, chargingConnector, core.ChargePointStatusAvailable) -} - -// Start function -func main() { - // Parse arguments from env variables - id, ok := os.LookupEnv("CLIENT_ID") - if !ok { - log.Print("Usage:\n\tocppClientId\n\tocppServerUrl") - return - } - csUrl, ok := os.LookupEnv("CENTRAL_SYSTEM_URL") - if !ok { - log.Print("Usage:\n\tocppClientId\n\tocppServerUrl") - return - } - // Create a default OCPP 1.6 charge point - chargePoint = ocpp16.NewChargePoint(id, nil, nil) - // Set a handler for all callback functions - connectors := map[int]*ConnectorInfo{ - 1: {status: core.ChargePointStatusAvailable, availability: core.AvailabilityTypeOperative, currentTransaction: 0}, - } - handler := &ChargePointHandler{ - status: core.ChargePointStatusAvailable, - connectors: connectors, - configuration: map[string]core.ConfigurationKey{}, - errorCode: core.NoError, - localAuthList: []localauth.AuthorizationData{}, - localAuthListVersion: 0} - chargePoint.SetChargePointCoreHandler(handler) - // Connects to central system - err := chargePoint.Start(csUrl) - if err != nil { - log.Println(err) - } else { - log.Infof("connected to central system at %v", csUrl) - exampleRoutine(chargePoint, handler) - // Disconnect - chargePoint.Stop() - log.Infof("disconnected from central system") - } -} - -func init() { - log.SetLevel(log.InfoLevel) -} - -// Utility functions -func logDefault(feature string) *log.Entry { - return log.WithField("message", feature) -} diff --git a/example/1.6/create-test-certificates.sh b/example/1.6/create-test-certificates.sh new file mode 100755 index 00000000..d67aac6e --- /dev/null +++ b/example/1.6/create-test-certificates.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +mkdir -p certs/cs +mkdir -p certs/cp +cd certs +# Create CA +openssl req -new -x509 -nodes -days 120 -extensions v3_ca -keyout ca.key -out ca.crt -subj "/CN=ocpp-go-example" +# Generate self-signed central-system certificate +openssl genrsa -out cs/central-system.key 4096 +openssl req -out cs/central-system.csr -key cs/central-system.key -new -subj "/CN=central-system" +openssl x509 -req -in cs/central-system.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out cs/central-system.crt -days 120 +# Generate self-signed charge-point certificate +openssl genrsa -out cp/charge-point.key 4096 +openssl req -new -out cp/charge-point.csr -key cp/charge-point.key -subj "/CN=charge-point" +openssl x509 -req -in cp/charge-point.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out cp/charge-point.crt -days 120 diff --git a/example/cs1.6/Dockerfile b/example/1.6/cs/Dockerfile similarity index 70% rename from example/cs1.6/Dockerfile rename to example/1.6/cs/Dockerfile index d6798263..856a96b7 100644 --- a/example/cs1.6/Dockerfile +++ b/example/1.6/cs/Dockerfile @@ -9,16 +9,17 @@ COPY . . # Fetch dependencies. RUN go mod download # Build the binary. -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/central_system example/cs1.6/central_system_sim.go +RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/central_system example/1.6/cs/*.go ############################ # STEP 2 build a small image ############################ -FROM scratch +FROM alpine COPY --from=builder /go/bin/central_system /bin/central_system -# Port on which the service will be exposed. +# Ports on which the service may be exposed. EXPOSE 8887 +EXPOSE 443 CMD [ "central_system" ] diff --git a/example/1.6/cs/central_system_sim.go b/example/1.6/cs/central_system_sim.go new file mode 100644 index 00000000..f7213ba4 --- /dev/null +++ b/example/1.6/cs/central_system_sim.go @@ -0,0 +1,205 @@ +package main + +import ( + ocpp16 "github.com/lorenzodonini/ocpp-go/ocpp1.6" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/firmware" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/localauth" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/remotetrigger" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/reservation" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" + "github.com/lorenzodonini/ocpp-go/ws" + log "github.com/sirupsen/logrus" + "os" + "strconv" + "time" +) + +const ( + defaultListenPort = 8887 + defaultHeartbeatInterval = 600 + envVarServerPort = "SERVER_LISTEN_PORT" + envVarTls = "TLS_ENABLED" + envVarServerCertificate = "SERVER_CERTIFICATE_PATH" + envVarServerCertificateKey = "SERVER_CERTIFICATE_KEY_PATH" +) + +var centralSystem ocpp16.CentralSystem + +func setupCentralSystem() ocpp16.CentralSystem { + return ocpp16.NewCentralSystem(nil, nil) +} + +func setupTlsCentralSystem() ocpp16.CentralSystem { + certificate, ok := os.LookupEnv(envVarServerCertificate) + if !ok { + log.Fatalf("no required %v found", envVarServerCertificate) + } + key, ok := os.LookupEnv(envVarServerCertificateKey) + if !ok { + log.Fatalf("no required %v found", envVarServerCertificateKey) + } + server := ws.NewTLSServer(certificate, key) + return ocpp16.NewCentralSystem(nil, server) +} + +// Run for every connected Charge Point, to simulate some functionality +func exampleRoutine(chargePointID string, handler *CentralSystemHandler) { + // Wait for some time + time.Sleep(2 * time.Second) + // Reserve a connector + reservationID := 42 + clientIdTag := "l33t" + connectorID := 1 + expiryDate := types.NewDateTime(time.Now().Add(1 * time.Hour)) + cb1 := func(confirmation *reservation.ReserveNowConfirmation, err error) { + if err != nil { + logDefault(chargePointID, confirmation.GetFeatureName()).Warn(err) + } else if confirmation.Status == reservation.ReservationStatusAccepted { + logDefault(chargePointID, confirmation.GetFeatureName()).Infof("connector %v reserved for client %v until %v (reservation ID %d)", connectorID, clientIdTag, expiryDate.FormatTimestamp(), reservationID) + } else { + logDefault(chargePointID, confirmation.GetFeatureName()).Infof("couldn't reserve connector %v: %v", connectorID, confirmation.Status) + } + } + e := centralSystem.ReserveNow(chargePointID, cb1, connectorID, expiryDate, clientIdTag, reservationID) + if e != nil { + logDefault(chargePointID, reservation.ReserveNowFeatureName).Errorf("couldn't send message: %v", e) + return + } + // Wait for some time + time.Sleep(1 * time.Second) + // Cancel the reservation + cb2 := func(confirmation *reservation.CancelReservationConfirmation, err error) { + if err != nil { + logDefault(chargePointID, confirmation.GetFeatureName()).Warn(err) + } else if confirmation.Status == reservation.CancelReservationStatusAccepted { + logDefault(chargePointID, confirmation.GetFeatureName()).Infof("reservation %v canceled successfully", reservationID) + } else { + logDefault(chargePointID, confirmation.GetFeatureName()).Infof("couldn't cancel reservation %v", reservationID) + } + } + e = centralSystem.CancelReservation(chargePointID, cb2, reservationID) + if e != nil { + logDefault(chargePointID, reservation.ReserveNowFeatureName).Errorf("couldn't send message: %v", e) + return + } + // Wait for some time + time.Sleep(5 * time.Second) + // Get current local list version + cb3 := func(confirmation *localauth.GetLocalListVersionConfirmation, err error) { + if err != nil { + logDefault(chargePointID, confirmation.GetFeatureName()).Errorf("error while sending request: %v", err) + } else { + logDefault(chargePointID, confirmation.GetFeatureName()).Infof("current local list version: %v", confirmation.ListVersion) + } + } + e = centralSystem.GetLocalListVersion(chargePointID, cb3) + if e != nil { + logDefault(chargePointID, localauth.GetLocalListVersionFeatureName).Errorf("couldn't send message: %v", e) + return + } + // Wait for some time + time.Sleep(5 * time.Second) + configKey := "MeterValueSampleInterval" + configValue := "10" + // Change meter sampling values time + cb4 := func(confirmation *core.ChangeConfigurationConfirmation, err error) { + if err != nil { + logDefault(chargePointID, confirmation.GetFeatureName()).Errorf("error while sending request: %v", err) + } else if confirmation.Status == core.ConfigurationStatusNotSupported { + logDefault(chargePointID, confirmation.GetFeatureName()).Warnf("couldn't update configuration for unsupported key: %v", configKey) + } else if confirmation.Status == core.ConfigurationStatusRejected { + logDefault(chargePointID, confirmation.GetFeatureName()).Warnf("couldn't update configuration for readonly key: %v", configKey) + } else { + logDefault(chargePointID, confirmation.GetFeatureName()).Infof("updated configuration for key %v to: %v", configKey, configValue) + } + } + e = centralSystem.ChangeConfiguration(chargePointID, cb4, configKey, configValue) + if e != nil { + logDefault(chargePointID, localauth.GetLocalListVersionFeatureName).Errorf("couldn't send message: %v", e) + return + } + + // Wait for some time + time.Sleep(5 * time.Second) + // Trigger a heartbeat message + cb5 := func(confirmation *remotetrigger.TriggerMessageConfirmation, err error) { + if err != nil { + logDefault(chargePointID, confirmation.GetFeatureName()).Errorf("error while sending request: %v", err) + } else if confirmation.Status == remotetrigger.TriggerMessageStatusAccepted { + logDefault(chargePointID, confirmation.GetFeatureName()).Infof("%v triggered successfully", core.HeartbeatFeatureName) + } else if confirmation.Status == remotetrigger.TriggerMessageStatusRejected { + logDefault(chargePointID, confirmation.GetFeatureName()).Infof("%v trigger was rejected", core.HeartbeatFeatureName) + } + } + e = centralSystem.TriggerMessage(chargePointID, cb5, core.HeartbeatFeatureName) + if e != nil { + logDefault(chargePointID, remotetrigger.TriggerMessageFeatureName).Errorf("couldn't send message: %v", e) + return + } + + // Wait for some time + time.Sleep(5 * time.Second) + // Trigger a diagnostics status notification + cb6 := func(confirmation *remotetrigger.TriggerMessageConfirmation, err error) { + if err != nil { + logDefault(chargePointID, confirmation.GetFeatureName()).Errorf("error while sending request: %v", err) + } else if confirmation.Status == remotetrigger.TriggerMessageStatusAccepted { + logDefault(chargePointID, confirmation.GetFeatureName()).Infof("%v triggered successfully", firmware.GetDiagnosticsFeatureName) + } else if confirmation.Status == remotetrigger.TriggerMessageStatusRejected { + logDefault(chargePointID, confirmation.GetFeatureName()).Infof("%v trigger was rejected", firmware.GetDiagnosticsFeatureName) + } + } + e = centralSystem.TriggerMessage(chargePointID, cb6, firmware.DiagnosticsStatusNotificationFeatureName) + if e != nil { + logDefault(chargePointID, remotetrigger.TriggerMessageFeatureName).Errorf("couldn't send message: %v", e) + return + } +} + +// Start function +func main() { + // Load config from ENV + var listenPort = defaultListenPort + port, _ := os.LookupEnv(envVarServerPort) + if p, err := strconv.Atoi(port); err == nil { + listenPort = p + } else { + log.Printf("no valid %v environment variable found, using default port", envVarServerPort) + } + // Check if TLS enabled + t, _ := os.LookupEnv(envVarTls) + tlsEnabled, _ := strconv.ParseBool(t) + // Prepare OCPP 1.6 central system + if tlsEnabled { + centralSystem = setupTlsCentralSystem() + } else { + centralSystem = setupCentralSystem() + } + // Support callbacks for all OCPP 1.6 profiles + handler := &CentralSystemHandler{chargePoints: map[string]*ChargePointState{}} + centralSystem.SetCoreHandler(handler) + centralSystem.SetLocalAuthListHandler(handler) + centralSystem.SetFirmwareManagementHandler(handler) + centralSystem.SetReservationHandler(handler) + centralSystem.SetRemoteTriggerHandler(handler) + centralSystem.SetSmartChargingHandler(handler) + // Add handlers for dis/connection of charge points + centralSystem.SetNewChargePointHandler(func(chargePointId string) { + handler.chargePoints[chargePointId] = &ChargePointState{connectors: map[int]*ConnectorInfo{}, transactions: map[int]*TransactionInfo{}} + log.WithField("client", chargePointId).Info("new charge point connected") + go exampleRoutine(chargePointId, handler) + }) + centralSystem.SetChargePointDisconnectedHandler(func(chargePointId string) { + log.WithField("client", chargePointId).Info("charge point disconnected") + delete(handler.chargePoints, chargePointId) + }) + // Run central system + log.Infof("starting central system on port %v", listenPort) + centralSystem.Start(listenPort, "/{ws}") + log.Info("stopped central system") +} + +func init() { + log.SetLevel(log.InfoLevel) +} diff --git a/example/cs1.6/central_system_sim.go b/example/1.6/cs/handler.go similarity index 82% rename from example/cs1.6/central_system_sim.go rename to example/1.6/cs/handler.go index 82641b0b..095c52b8 100644 --- a/example/cs1.6/central_system_sim.go +++ b/example/1.6/cs/handler.go @@ -2,28 +2,18 @@ package main import ( "fmt" - ocpp16 "github.com/lorenzodonini/ocpp-go/ocpp1.6" "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/lorenzodonini/ocpp-go/ocpp1.6/firmware" "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" log "github.com/sirupsen/logrus" - "os" - "strconv" "time" ) -const ( - defaultListenPort = 8887 - defaultHeartbeatInterval = 600 -) - var ( nextTransactionId = 0 ) -//TODO: cache authorization - -// Charge Point state +// TransactionInfo contains info about a transaction type TransactionInfo struct { id int startTime *types.DateTime @@ -38,6 +28,7 @@ func (ti *TransactionInfo) hasTransactionEnded() bool { return ti.endTime != nil && !ti.endTime.IsZero() } +// ConnectorInfo contains status and ongoing transaction ID for a connector type ConnectorInfo struct { status core.ChargePointStatus currentTransaction int @@ -47,6 +38,7 @@ func (ci *ConnectorInfo) hasTransactionInProgress() bool { return ci.currentTransaction >= 0 } +// ChargePointState contains some simple state for a connected charge point type ChargePointState struct { status core.ChargePointStatus diagnosticsStatus firmware.DiagnosticsStatus @@ -65,11 +57,14 @@ func (cps *ChargePointState) getConnector(id int) *ConnectorInfo { return ci } +// CentralSystemHandler contains some simple state that a central system may want to keep. +// In production this will typically be replaced by database/API calls. type CentralSystemHandler struct { chargePoints map[string]*ChargePointState } -// Core profile callbacks +// ------------- Core profile callbacks ------------- + func (handler *CentralSystemHandler) OnAuthorize(chargePointId string, request *core.AuthorizeRequest) (confirmation *core.AuthorizeConfirmation, err error) { logDefault(chargePointId, request.GetFeatureName()).Infof("client authorized") return core.NewAuthorizationConfirmation(types.NewIdTagInfo(types.AuthorizationStatusAccepted)), nil @@ -86,13 +81,14 @@ func (handler *CentralSystemHandler) OnDataTransfer(chargePointId string, reques } func (handler *CentralSystemHandler) OnHeartbeat(chargePointId string, request *core.HeartbeatRequest) (confirmation *core.HeartbeatConfirmation, err error) { + logDefault(chargePointId, request.GetFeatureName()).Infof("heartbeat handled") return core.NewHeartbeatConfirmation(types.NewDateTime(time.Now())), nil } func (handler *CentralSystemHandler) OnMeterValues(chargePointId string, request *core.MeterValuesRequest) (confirmation *core.MeterValuesConfirmation, err error) { logDefault(chargePointId, request.GetFeatureName()).Infof("received meter values for connector %v, transaction %v. Meter values:\n", request.ConnectorId, request.TransactionId) for _, mv := range request.MeterValue { - logDefault(chargePointId, request.GetFeatureName()).Printf("\t %v", mv) + logDefault(chargePointId, request.GetFeatureName()).Printf("%v", mv) } return core.NewMeterValuesConfirmation(), nil } @@ -148,16 +144,17 @@ func (handler *CentralSystemHandler) OnStopTransaction(chargePointId string, req connector.currentTransaction = -1 transaction.endTime = request.Timestamp transaction.endMeter = request.MeterStop - //TODO: meter data + //TODO: bill charging period to client } - logDefault(chargePointId, request.GetFeatureName()).Infof("stopped transaction %v - %v. Meter values:", request.TransactionId, request.Reason) + logDefault(chargePointId, request.GetFeatureName()).Infof("stopped transaction %v - %v", request.TransactionId, request.Reason) for _, mv := range request.TransactionData { - logDefault(chargePointId, request.GetFeatureName()).Printf("\t %v", mv) + logDefault(chargePointId, request.GetFeatureName()).Printf("%v", mv) } return core.NewStopTransactionConfirmation(), nil } -// Firmware management callbacks +// ------------- Firmware management profile callbacks ------------- + func (handler *CentralSystemHandler) OnDiagnosticsStatusNotification(chargePointId string, request *firmware.DiagnosticsStatusNotificationRequest) (confirmation *firmware.DiagnosticsStatusNotificationConfirmation, err error) { info, ok := handler.chargePoints[chargePointId] if !ok { @@ -180,37 +177,8 @@ func (handler *CentralSystemHandler) OnFirmwareStatusNotification(chargePointId // No callbacks for Local Auth management, Reservation, Remote trigger or Smart Charging profile on central system -// Start function -func main() { - args := os.Args[1:] - centralSystem := ocpp16.NewCentralSystem(nil, nil) - handler := &CentralSystemHandler{chargePoints: map[string]*ChargePointState{}} - centralSystem.SetNewChargePointHandler(func(chargePointId string) { - handler.chargePoints[chargePointId] = &ChargePointState{connectors: map[int]*ConnectorInfo{}, transactions: map[int]*TransactionInfo{}} - log.WithField("client", chargePointId).Info("new charge point connected") - }) - centralSystem.SetChargePointDisconnectedHandler(func(chargePointId string) { - log.WithField("client", chargePointId).Info("charge point disconnected") - delete(handler.chargePoints, chargePointId) - }) - centralSystem.SetCentralSystemCoreHandler(handler) - var listenPort = defaultListenPort - if len(args) > 0 { - port, err := strconv.Atoi(args[0]) - if err != nil { - listenPort = port - } - } - log.Infof("starting central system on port %v", listenPort) - centralSystem.Start(listenPort, "/{ws}") - log.Info("stopped central system") -} - // Utility functions + func logDefault(chargePointId string, feature string) *log.Entry { return log.WithFields(log.Fields{"client": chargePointId, "message": feature}) -} - -func init() { - log.SetLevel(log.InfoLevel) -} +} \ No newline at end of file diff --git a/example/1.6/docker-compose.tls.yml b/example/1.6/docker-compose.tls.yml new file mode 100644 index 00000000..fa1e3025 --- /dev/null +++ b/example/1.6/docker-compose.tls.yml @@ -0,0 +1,42 @@ +version: '3' +services: + central-system: + build: + context: ../.. + dockerfile: cs/Dockerfile + image: ldonini/ocpp1.6-central-system:latest + container_name: central-system + volumes: + - ./certs/cs:/usr/local/share/certs + - ./certs/ca.crt:/usr/local/share/certs/ca.crt + environment: + - SERVER_LISTEN_PORT=443 + - TLS_ENABLED=true + - SERVER_CERTIFICATE_PATH=/usr/local/share/certs/central-system.crt + - SERVER_CERTIFICATE_KEY_PATH=/usr/local/share/certs/central-system.key + ports: + - "443:443" + networks: + - sim + charge-point: + build: + context: ../.. + dockerfile: cp/Dockerfile + image: ldonini/ocpp1.6-charge-point:latest + container_name: charge-point + volumes: + - ./certs/cp:/usr/local/share/certs + - ./certs/ca.crt:/usr/local/share/certs/ca.crt + environment: + - CLIENT_ID=chargePointSim + - CENTRAL_SYSTEM_URL=wss://central-system:443 + - TLS_ENABLED=true + - CA_CERTIFICATE_PATH=/usr/local/share/certs/ca.crt + - CLIENT_CERTIFICATE_PATH=/usr/local/share/certs/charge-point.crt + - CLIENT_CERTIFICATE_KEY_PATH=/usr/local/share/certs/charge-point.key + networks: + - sim + +networks: + sim: + driver: bridge diff --git a/docker-compose.yml b/example/1.6/docker-compose.yml similarity index 61% rename from docker-compose.yml rename to example/1.6/docker-compose.yml index 5adbc416..cb96729f 100644 --- a/docker-compose.yml +++ b/example/1.6/docker-compose.yml @@ -1,24 +1,26 @@ version: '3' services: - central_system: + central-system: build: - context: . - dockerfile: example/cs1.6/Dockerfile + context: ../.. + dockerfile: cs/Dockerfile image: ldonini/ocpp1.6-central-system:latest container_name: central-system + environment: + - SERVER_LISTEN_PORT=8887 ports: - "8887:8887" networks: - sim - charge_point: - environment: - - CLIENT_ID=chargePointSim - - CENTRAL_SYSTEM_URL=ws://central_system:8887 + charge-point: build: - context: . - dockerfile: example/cp1.6/Dockerfile + context: ../.. + dockerfile: cp/Dockerfile image: ldonini/ocpp1.6-charge-point:latest container_name: charge-point + environment: + - CLIENT_ID=chargePointSim + - CENTRAL_SYSTEM_URL=ws://central-system:8887 networks: - sim diff --git a/ocpp1.6/central_system.go b/ocpp1.6/central_system.go index 00ef7ecf..ccfa8af8 100644 --- a/ocpp1.6/central_system.go +++ b/ocpp1.6/central_system.go @@ -16,12 +16,12 @@ import ( type centralSystem struct { server *ocppj.Server - coreHandler core.CentralSystemCoreHandler - localAuthListHandler localauth.CentralSystemLocalAuthListHandler - firmwareHandler firmware.CentralSystemFirmwareManagementHandler - reservationHandler reservation.CentralSystemReservationHandler - remoteTriggerHandler remotetrigger.CentralSystemRemoteTriggerHandler - smartChargingHandler smartcharging.CentralSystemSmartChargingHandler + coreHandler core.CentralSystemHandler + localAuthListHandler localauth.CentralSystemHandler + firmwareHandler firmware.CentralSystemHandler + reservationHandler reservation.CentralSystemHandler + remoteTriggerHandler remotetrigger.CentralSystemHandler + smartChargingHandler smartcharging.CentralSystemHandler callbacks map[string]func(confirmation ocpp.Response, err error) } @@ -310,28 +310,28 @@ func (cs *centralSystem) GetCompositeSchedule(clientId string, callback func(*sm return cs.SendRequestAsync(clientId, request, genericCallback) } -func (cs *centralSystem) SetCentralSystemCoreHandler(listener core.CentralSystemCoreHandler) { - cs.coreHandler = listener +func (cs *centralSystem) SetCoreHandler(handler core.CentralSystemHandler) { + cs.coreHandler = handler } -func (cs *centralSystem) SetLocalAuthListHandler(listener localauth.CentralSystemLocalAuthListHandler) { - cs.localAuthListHandler = listener +func (cs *centralSystem) SetLocalAuthListHandler(handler localauth.CentralSystemHandler) { + cs.localAuthListHandler = handler } -func (cs *centralSystem) SetFirmwareManagementHandler(listener firmware.CentralSystemFirmwareManagementHandler) { - cs.firmwareHandler = listener +func (cs *centralSystem) SetFirmwareManagementHandler(handler firmware.CentralSystemHandler) { + cs.firmwareHandler = handler } -func (cs *centralSystem) SetReservationHandler(listener reservation.CentralSystemReservationHandler) { - cs.reservationHandler = listener +func (cs *centralSystem) SetReservationHandler(handler reservation.CentralSystemHandler) { + cs.reservationHandler = handler } -func (cs *centralSystem) SetRemoteTriggerHandler(listener remotetrigger.CentralSystemRemoteTriggerHandler) { - cs.remoteTriggerHandler = listener +func (cs *centralSystem) SetRemoteTriggerHandler(handler remotetrigger.CentralSystemHandler) { + cs.remoteTriggerHandler = handler } -func (cs *centralSystem) SetSmartChargingHandler(listener smartcharging.CentralSystemSmartChargingHandler) { - cs.smartChargingHandler = listener +func (cs *centralSystem) SetSmartChargingHandler(handler smartcharging.CentralSystemHandler) { + cs.smartChargingHandler = handler } func (cs *centralSystem) SetNewChargePointHandler(handler func(chargePointId string)) { @@ -412,7 +412,7 @@ func (cs *centralSystem) notSupportedError(chargePointId string, requestId strin func (cs *centralSystem) handleIncomingRequest(chargePointId string, request ocpp.Request, requestId string, action string) { profile, found := cs.server.GetProfileForFeature(action) - // Check whether action is supported and a listener for it exists + // Check whether action is supported and a handler for it exists if !found { cs.notImplementedError(chargePointId, requestId, action) return diff --git a/ocpp1.6/charge_point.go b/ocpp1.6/charge_point.go index 5f34ad17..6f2b431f 100644 --- a/ocpp1.6/charge_point.go +++ b/ocpp1.6/charge_point.go @@ -16,12 +16,12 @@ import ( type chargePoint struct { client *ocppj.Client - coreHandler core.ChargePointCoreHandler - localAuthListHandler localauth.ChargePointLocalAuthListHandler - firmwareHandler firmware.ChargePointFirmwareManagementHandler - reservationHandler reservation.ChargePointReservationHandler - remoteTriggerHandler remotetrigger.ChargePointRemoteTriggerHandler - smartChargingHandler smartcharging.ChargePointSmartChargingHandler + coreHandler core.ChargePointHandler + localAuthListHandler localauth.ChargePointHandler + firmwareHandler firmware.ChargePointHandler + reservationHandler reservation.ChargePointHandler + remoteTriggerHandler remotetrigger.ChargePointHandler + smartChargingHandler smartcharging.ChargePointHandler confirmationHandler chan ocpp.Response errorHandler chan error } @@ -156,28 +156,28 @@ func (cp *chargePoint) FirmwareStatusNotification(status firmware.FirmwareStatus } } -func (cp *chargePoint) SetChargePointCoreHandler(listener core.ChargePointCoreHandler) { - cp.coreHandler = listener +func (cp *chargePoint) SetCoreHandler(handler core.ChargePointHandler) { + cp.coreHandler = handler } -func (cp *chargePoint) SetLocalAuthListHandler(listener localauth.ChargePointLocalAuthListHandler) { - cp.localAuthListHandler = listener +func (cp *chargePoint) SetLocalAuthListHandler(handler localauth.ChargePointHandler) { + cp.localAuthListHandler = handler } -func (cp *chargePoint) SetFirmwareManagementHandler(listener firmware.ChargePointFirmwareManagementHandler) { - cp.firmwareHandler = listener +func (cp *chargePoint) SetFirmwareManagementHandler(handler firmware.ChargePointHandler) { + cp.firmwareHandler = handler } -func (cp *chargePoint) SetReservationHandler(listener reservation.ChargePointReservationHandler) { - cp.reservationHandler = listener +func (cp *chargePoint) SetReservationHandler(handler reservation.ChargePointHandler) { + cp.reservationHandler = handler } -func (cp *chargePoint) SetRemoteTriggerHandler(listener remotetrigger.ChargePointRemoteTriggerHandler) { - cp.remoteTriggerHandler = listener +func (cp *chargePoint) SetRemoteTriggerHandler(handler remotetrigger.ChargePointHandler) { + cp.remoteTriggerHandler = handler } -func (cp *chargePoint) SetSmartChargingHandler(listener smartcharging.ChargePointSmartChargingHandler) { - cp.smartChargingHandler = listener +func (cp *chargePoint) SetSmartChargingHandler(handler smartcharging.ChargePointHandler) { + cp.smartChargingHandler = handler } func (cp *chargePoint) SendRequest(request ocpp.Request) (ocpp.Response, error) { @@ -267,7 +267,7 @@ func (cp *chargePoint) notSupportedError(requestId string, action string) { func (cp *chargePoint) handleIncomingRequest(request ocpp.Request, requestId string, action string) { profile, found := cp.client.GetProfileForFeature(action) - // Check whether action is supported and a listener for it exists + // Check whether action is supported and a handler for it exists if !found { cp.notImplementedError(requestId, action) return diff --git a/ocpp1.6/core/core.go b/ocpp1.6/core/core.go index cf6e0d98..fdc3f95f 100644 --- a/ocpp1.6/core/core.go +++ b/ocpp1.6/core/core.go @@ -6,7 +6,7 @@ import ( ) // Needs to be implemented by Central systems for handling messages part of the OCPP 1.6 Core profile. -type CentralSystemCoreHandler interface { +type CentralSystemHandler interface { OnAuthorize(chargePointId string, request *AuthorizeRequest) (confirmation *AuthorizeConfirmation, err error) OnBootNotification(chargePointId string, request *BootNotificationRequest) (confirmation *BootNotificationConfirmation, err error) OnDataTransfer(chargePointId string, request *DataTransferRequest) (confirmation *DataTransferConfirmation, err error) @@ -18,7 +18,7 @@ type CentralSystemCoreHandler interface { } // Needs to be implemented by Charge points for handling messages part of the OCPP 1.6 Core profile. -type ChargePointCoreHandler interface { +type ChargePointHandler interface { OnChangeAvailability(request *ChangeAvailabilityRequest) (confirmation *ChangeAvailabilityConfirmation, err error) OnChangeConfiguration(request *ChangeConfigurationRequest) (confirmation *ChangeConfigurationConfirmation, err error) OnClearCache(request *ClearCacheRequest) (confirmation *ClearCacheConfirmation, err error) diff --git a/ocpp1.6/firmware/firmware.go b/ocpp1.6/firmware/firmware.go index 743fc5da..b7862fa9 100644 --- a/ocpp1.6/firmware/firmware.go +++ b/ocpp1.6/firmware/firmware.go @@ -6,13 +6,13 @@ import ( ) // Needs to be implemented by Central systems for handling messages part of the OCPP 1.6 FirmwareManagement profile. -type CentralSystemFirmwareManagementHandler interface { +type CentralSystemHandler interface { OnDiagnosticsStatusNotification(chargePointId string, request *DiagnosticsStatusNotificationRequest) (confirmation *DiagnosticsStatusNotificationConfirmation, err error) OnFirmwareStatusNotification(chargePointId string, request *FirmwareStatusNotificationRequest) (confirmation *FirmwareStatusNotificationConfirmation, err error) } // Needs to be implemented by Charge points for handling messages part of the OCPP 1.6 FirmwareManagement profile. -type ChargePointFirmwareManagementHandler interface { +type ChargePointHandler interface { OnGetDiagnostics(request *GetDiagnosticsRequest) (confirmation *GetDiagnosticsConfirmation, err error) OnUpdateFirmware(request *UpdateFirmwareRequest) (confirmation *UpdateFirmwareConfirmation, err error) } diff --git a/ocpp1.6/localauth/local_auth_list.go b/ocpp1.6/localauth/local_auth_list.go index a87abf25..c3b89c5f 100644 --- a/ocpp1.6/localauth/local_auth_list.go +++ b/ocpp1.6/localauth/local_auth_list.go @@ -4,11 +4,11 @@ package localauth import "github.com/lorenzodonini/ocpp-go/ocpp" // Needs to be implemented by Central systems for handling messages part of the OCPP 1.6 LocalAuthList profile. -type CentralSystemLocalAuthListHandler interface { +type CentralSystemHandler interface { } // Needs to be implemented by Charge points for handling messages part of the OCPP 1.6 LocalAuthList profile. -type ChargePointLocalAuthListHandler interface { +type ChargePointHandler interface { OnGetLocalListVersion(request *GetLocalListVersionRequest) (confirmation *GetLocalListVersionConfirmation, err error) OnSendLocalList(request *SendLocalListRequest) (confirmation *SendLocalListConfirmation, err error) } diff --git a/ocpp1.6/remotetrigger/remote_trigger.go b/ocpp1.6/remotetrigger/remote_trigger.go index d64a658b..721aa97a 100644 --- a/ocpp1.6/remotetrigger/remote_trigger.go +++ b/ocpp1.6/remotetrigger/remote_trigger.go @@ -4,11 +4,11 @@ package remotetrigger import "github.com/lorenzodonini/ocpp-go/ocpp" // Needs to be implemented by Central systems for handling messages part of the OCPP 1.6 RemoteTrigger profile. -type CentralSystemRemoteTriggerHandler interface { +type CentralSystemHandler interface { } // Needs to be implemented by Charge points for handling messages part of the OCPP 1.6 RemoteTrigger profile. -type ChargePointRemoteTriggerHandler interface { +type ChargePointHandler interface { OnTriggerMessage(request *TriggerMessageRequest) (confirmation *TriggerMessageConfirmation, err error) } diff --git a/ocpp1.6/reservation/reservation.go b/ocpp1.6/reservation/reservation.go index 023faa58..43ce30e1 100644 --- a/ocpp1.6/reservation/reservation.go +++ b/ocpp1.6/reservation/reservation.go @@ -4,11 +4,11 @@ package reservation import "github.com/lorenzodonini/ocpp-go/ocpp" // Needs to be implemented by Central systems for handling messages part of the OCPP 1.6 Reservation profile. -type CentralSystemReservationHandler interface { +type CentralSystemHandler interface { } // Needs to be implemented by Charge points for handling messages part of the OCPP 1.6 Reservation profile. -type ChargePointReservationHandler interface { +type ChargePointHandler interface { OnReserveNow(request *ReserveNowRequest) (confirmation *ReserveNowConfirmation, err error) OnCancelReservation(request *CancelReservationRequest) (confirmation *CancelReservationConfirmation, err error) } diff --git a/ocpp1.6/smartcharging/smart_charging.go b/ocpp1.6/smartcharging/smart_charging.go index 24792a01..2b7a06f5 100644 --- a/ocpp1.6/smartcharging/smart_charging.go +++ b/ocpp1.6/smartcharging/smart_charging.go @@ -4,11 +4,11 @@ package smartcharging import "github.com/lorenzodonini/ocpp-go/ocpp" // Needs to be implemented by Central systems for handling messages part of the OCPP 1.6 SmartCharging profile. -type CentralSystemSmartChargingHandler interface { +type CentralSystemHandler interface { } // Needs to be implemented by Charge points for handling messages part of the OCPP 1.6 SmartCharging profile. -type ChargePointSmartChargingHandler interface { +type ChargePointHandler interface { OnSetChargingProfile(request *SetChargingProfileRequest) (confirmation *SetChargingProfileConfirmation, err error) OnClearChargingProfile(request *ClearChargingProfileRequest) (confirmation *ClearChargingProfileConfirmation, err error) OnGetCompositeSchedule(request *GetCompositeScheduleRequest) (confirmation *GetCompositeScheduleConfirmation, err error) diff --git a/ocpp1.6/v16.go b/ocpp1.6/v16.go index fb13c15a..df6b17b4 100644 --- a/ocpp1.6/v16.go +++ b/ocpp1.6/v16.go @@ -23,8 +23,8 @@ import ( // // The logic for incoming messages needs to be implemented, and the message handlers need to be registered with the charge point: // handler := &ChargePointHandler{} -// client.SetChargePointCoreHandler(handler) -// Refer to the ChargePointCoreHandler, ChargePointFirmwareHandler, ChargePointLocalAuthListHandler, ChargePointReservationHandler, ChargePointRemoteTriggerHandler and ChargePointSmartChargingHandlers interfaces for the implementation requirements. +// client.SetCoreHandler(handler) +// Refer to the ChargePointHandler interfaces in the respective core, firmware, localauth, remotetrigger, reservation and smartcharging profiles for the implementation requirements. // // A charge point can be started and stopped using the Start and Stop functions. // While running, messages can be sent to the Central system by calling the Charge point's functions, e.g. @@ -55,17 +55,17 @@ type ChargePoint interface { FirmwareStatusNotification(status firmware.FirmwareStatus, props ...func(request *firmware.FirmwareStatusNotificationRequest)) (*firmware.FirmwareStatusNotificationConfirmation, error) // Registers a handler for incoming core profile messages - SetChargePointCoreHandler(listener core.ChargePointCoreHandler) + SetCoreHandler(listener core.ChargePointHandler) // Registers a handler for incoming local authorization profile messages - SetLocalAuthListHandler(listener localauth.ChargePointLocalAuthListHandler) + SetLocalAuthListHandler(listener localauth.ChargePointHandler) // Registers a handler for incoming firmware management profile messages - SetFirmwareManagementHandler(listener firmware.ChargePointFirmwareManagementHandler) + SetFirmwareManagementHandler(listener firmware.ChargePointHandler) // Registers a handler for incoming reservation profile messages - SetReservationHandler(listener reservation.ChargePointReservationHandler) + SetReservationHandler(listener reservation.ChargePointHandler) // Registers a handler for incoming remote trigger profile messages - SetRemoteTriggerHandler(listener remotetrigger.ChargePointRemoteTriggerHandler) + SetRemoteTriggerHandler(listener remotetrigger.ChargePointHandler) // Registers a handler for incoming smart charging profile messages - SetSmartChargingHandler(listener smartcharging.ChargePointSmartChargingHandler) + SetSmartChargingHandler(listener smartcharging.ChargePointHandler) // Sends a request to the central system. // The central system will respond with a confirmation, or with an error if the request was invalid or could not be processed. // In case of network issues (i.e. the remote host couldn't be reached), the function also returns an error. @@ -151,8 +151,8 @@ func NewChargePoint(id string, dispatcher *ocppj.Client, client ws.WsClient) Cha // // The logic for handling incoming messages needs to be implemented, and the message handlers need to be registered with the central system: // handler := &CentralSystemHandler{} -// server.SetCentralSystemCoreHandler(handler) -// // Refer to the CentralSystemCoreHandler, CentralSystemFirmwareHandler, CentralSystemLocalAuthListHandler, CentralSystemReservationHandler, CentralSystemRemoteTriggerHandler and CentralSystemSmartChargingHandlers interfaces for the implementation requirements. +// server.SetCoreHandler(handler) +// Refer to the CentralSystemHandler interfaces in the respective core, firmware, localauth, remotetrigger, reservation and smartcharging profiles for the implementation requirements. // // A Central system can be started by using the Start function. // To be notified of incoming (dis)connections from charge points refer to the SetNewClientHandler and SetChargePointDisconnectedHandler functions. @@ -206,17 +206,17 @@ type CentralSystem interface { GetCompositeSchedule(clientId string, callback func(*smartcharging.GetCompositeScheduleConfirmation, error), connectorId int, duration int, props ...func(request *smartcharging.GetCompositeScheduleRequest)) error // Registers a handler for incoming core profile messages. - SetCentralSystemCoreHandler(listener core.CentralSystemCoreHandler) + SetCoreHandler(handler core.CentralSystemHandler) // Registers a handler for incoming local authorization profile messages. - SetLocalAuthListHandler(listener localauth.CentralSystemLocalAuthListHandler) + SetLocalAuthListHandler(handler localauth.CentralSystemHandler) // Registers a handler for incoming firmware management profile messages. - SetFirmwareManagementHandler(listener firmware.CentralSystemFirmwareManagementHandler) + SetFirmwareManagementHandler(handler firmware.CentralSystemHandler) // Registers a handler for incoming reservation profile messages. - SetReservationHandler(listener reservation.CentralSystemReservationHandler) + SetReservationHandler(handler reservation.CentralSystemHandler) // Registers a handler for incoming remote trigger profile messages. - SetRemoteTriggerHandler(listener remotetrigger.CentralSystemRemoteTriggerHandler) + SetRemoteTriggerHandler(handler remotetrigger.CentralSystemHandler) // Registers a handler for incoming smart charging profile messages. - SetSmartChargingHandler(listener smartcharging.CentralSystemSmartChargingHandler) + SetSmartChargingHandler(handler smartcharging.CentralSystemHandler) // Registers a handler for new incoming charge point connections. SetNewChargePointHandler(handler func(chargePointId string)) // Registers a handler for charge point disconnections. diff --git a/ocpp1.6_test/ocpp16_test.go b/ocpp1.6_test/ocpp16_test.go index 947d328b..77acdf39 100644 --- a/ocpp1.6_test/ocpp16_test.go +++ b/ocpp1.6_test/ocpp16_test.go @@ -439,12 +439,12 @@ type expectedChargePointOptions struct { forwardWrittenMessage bool } -func setupDefaultCentralSystemHandlers(suite *OcppV16TestSuite, coreListener core.CentralSystemCoreHandler, options expectedCentralSystemOptions) { +func setupDefaultCentralSystemHandlers(suite *OcppV16TestSuite, coreListener core.CentralSystemHandler, options expectedCentralSystemOptions) { t := suite.T() suite.centralSystem.SetNewChargePointHandler(func(chargePointId string) { assert.Equal(t, options.clientId, chargePointId) }) - suite.centralSystem.SetCentralSystemCoreHandler(coreListener) + suite.centralSystem.SetCoreHandler(coreListener) suite.mockWsServer.On("Start", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(options.startReturnArgument) suite.mockWsServer.On("Write", mock.AnythingOfType("string"), mock.Anything).Return(options.writeReturnArgument).Run(func(args mock.Arguments) { clientId := args.String(0) @@ -463,9 +463,9 @@ func setupDefaultCentralSystemHandlers(suite *OcppV16TestSuite, coreListener cor }) } -func setupDefaultChargePointHandlers(suite *OcppV16TestSuite, coreListener core.ChargePointCoreHandler, options expectedChargePointOptions) { +func setupDefaultChargePointHandlers(suite *OcppV16TestSuite, coreListener core.ChargePointHandler, options expectedChargePointOptions) { t := suite.T() - suite.chargePoint.SetChargePointCoreHandler(coreListener) + suite.chargePoint.SetCoreHandler(coreListener) suite.mockWsClient.On("Start", mock.AnythingOfType("string")).Return(options.startReturnArgument).Run(func(args mock.Arguments) { u := args.String(0) assert.Equal(t, fmt.Sprintf("%s/%s", options.serverUrl, options.clientId), u) diff --git a/ocpp2.0/authorization/authorization.go b/ocpp2.0/authorization/authorization.go index 0872d724..14a0b595 100644 --- a/ocpp2.0/authorization/authorization.go +++ b/ocpp2.0/authorization/authorization.go @@ -21,4 +21,4 @@ var Profile = ocpp.NewProfile( ProfileName, AuthorizeFeature{}, ClearCacheFeature{}, - ) +) diff --git a/ocpp2.0/availability/availability.go b/ocpp2.0/availability/availability.go index 33defcfe..ded8a210 100644 --- a/ocpp2.0/availability/availability.go +++ b/ocpp2.0/availability/availability.go @@ -19,4 +19,4 @@ const ProfileName = "availability" var Profile = ocpp.NewProfile( ProfileName, ChangeAvailabilityFeature{}, - ) +) diff --git a/ocpp2.0/data/data.go b/ocpp2.0/data/data.go index 15dcd04b..fa74f6fa 100644 --- a/ocpp2.0/data/data.go +++ b/ocpp2.0/data/data.go @@ -20,4 +20,4 @@ const ProfileName = "data" var Profile = ocpp.NewProfile( ProfileName, DataTransferFeature{}, - ) +) diff --git a/ocpp2.0/diagnostics/diagnostics.go b/ocpp2.0/diagnostics/diagnostics.go index 886d9fc8..ff258332 100644 --- a/ocpp2.0/diagnostics/diagnostics.go +++ b/ocpp2.0/diagnostics/diagnostics.go @@ -27,4 +27,4 @@ var Profile = ocpp.NewProfile( CustomerInformationFeature{}, GetLogFeature{}, GetMonitoringReportFeature{}, - ) +) diff --git a/ocpp2.0/display/display.go b/ocpp2.0/display/display.go index 92c2048a..177c4de0 100644 --- a/ocpp2.0/display/display.go +++ b/ocpp2.0/display/display.go @@ -21,4 +21,4 @@ var Profile = ocpp.NewProfile( ProfileName, ClearDisplayFeature{}, GetDisplayMessagesFeature{}, - ) +) diff --git a/ocpp2.0/firmware/firmware.go b/ocpp2.0/firmware/firmware.go index b3b98f8b..22c3d62c 100644 --- a/ocpp2.0/firmware/firmware.go +++ b/ocpp2.0/firmware/firmware.go @@ -18,4 +18,4 @@ const ProfileName = "firmware" var Profile = ocpp.NewProfile( ProfileName, FirmwareStatusNotificationFeature{}, - ) +) diff --git a/ocpp2.0/iso15118/iso_15118.go b/ocpp2.0/iso15118/iso_15118.go index 5315f6ba..f61bbf3b 100644 --- a/ocpp2.0/iso15118/iso_15118.go +++ b/ocpp2.0/iso15118/iso_15118.go @@ -31,4 +31,4 @@ var Profile = ocpp.NewProfile( Get15118EVCertificateFeature{}, GetCertificateStatusFeature{}, GetInstalledCertificateIdsFeature{}, - ) +) diff --git a/ocpp2.0/localauth/local_auth_list.go b/ocpp2.0/localauth/local_auth_list.go index 0f22633f..d62455fc 100644 --- a/ocpp2.0/localauth/local_auth_list.go +++ b/ocpp2.0/localauth/local_auth_list.go @@ -19,4 +19,4 @@ const ProfileName = "localAuthList" var Profile = ocpp.NewProfile( ProfileName, GetLocalListVersionFeature{}, - ) +) diff --git a/ocpp2.0/reservation/reservation.go b/ocpp2.0/reservation/reservation.go index 9298322b..bdb347cd 100644 --- a/ocpp2.0/reservation/reservation.go +++ b/ocpp2.0/reservation/reservation.go @@ -20,4 +20,4 @@ const ProfileName = "reservation" var Profile = ocpp.NewProfile( ProfileName, CancelReservationFeature{}, - ) +) diff --git a/ocpp2.0/smartcharging/smart_charging.go b/ocpp2.0/smartcharging/smart_charging.go index dba94e6e..05911a4f 100644 --- a/ocpp2.0/smartcharging/smart_charging.go +++ b/ocpp2.0/smartcharging/smart_charging.go @@ -29,4 +29,4 @@ var Profile = ocpp.NewProfile( ClearedChargingLimitFeature{}, GetChargingProfilesFeature{}, GetCompositeScheduleFeature{}, - ) +) diff --git a/ocpp2.0/tariffcost/tariff_cost.go b/ocpp2.0/tariffcost/tariff_cost.go index a1c9ffd6..3da76d88 100644 --- a/ocpp2.0/tariffcost/tariff_cost.go +++ b/ocpp2.0/tariffcost/tariff_cost.go @@ -18,4 +18,4 @@ const ProfileName = "tariffCost" var Profile = ocpp.NewProfile( ProfileName, CostUpdatedFeature{}, - ) +) diff --git a/ocppj_test/central_system_test.go b/ocppj/central_system_test.go similarity index 100% rename from ocppj_test/central_system_test.go rename to ocppj/central_system_test.go diff --git a/ocppj_test/charge_point_test.go b/ocppj/charge_point_test.go similarity index 100% rename from ocppj_test/charge_point_test.go rename to ocppj/charge_point_test.go diff --git a/ocppj_test/ocppj_test.go b/ocppj/ocppj_test.go similarity index 100% rename from ocppj_test/ocppj_test.go rename to ocppj/ocppj_test.go