Skip to content

Commit 178648e

Browse files
committed
feat: updates & docs (wip)
1 parent 3f5ee92 commit 178648e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+2192
-190
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
############################
2+
# STEP 1 build executable binary
3+
############################
4+
FROM golang:alpine AS builder
5+
6+
ENV GO111MODULE on
7+
WORKDIR $GOPATH/src/github.com/lorenzodonini/ocpp-go
8+
COPY . .
9+
# Fetch dependencies.
10+
RUN go mod download
11+
# Build the binary.
12+
RUN go build -ldflags="-w -s" -o /go/bin/charging_station example/2.0.1/chargingstation/*.go
13+
14+
############################
15+
# STEP 2 build a small image
16+
############################
17+
FROM alpine
18+
19+
COPY --from=builder /go/bin/charging_station /bin/charging_station
20+
21+
# Add CA certificates
22+
# It currently throws a warning on alpine: WARNING: ca-certificates.crt does not contain exactly one certificate or CRL: skipping.
23+
# Ignore the warning.
24+
RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/* && update-ca-certificates
25+
26+
CMD [ "charging_station" ]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package main
2+
3+
import (
4+
"github.com/lorenzodonini/ocpp-go/ocpp2.0.1/authorization"
5+
)
6+
7+
func (handler *ChargingStationHandler) OnClearCache(request *authorization.ClearCacheRequest) (response *authorization.ClearCacheResponse, err error) {
8+
logDefault(request.GetFeatureName()).Infof("cleared mocked cache")
9+
return authorization.NewClearCacheResponse(authorization.ClearCacheStatusAccepted), nil
10+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package main
2+
3+
import (
4+
"github.com/lorenzodonini/ocpp-go/ocpp2.0.1/availability"
5+
)
6+
7+
func (handler *ChargingStationHandler) OnChangeAvailability(request *availability.ChangeAvailabilityRequest) (response *availability.ChangeAvailabilityResponse, err error) {
8+
if request.Evse == nil {
9+
// Changing availability for the entire charging station
10+
handler.availability = request.OperationalStatus
11+
// TODO: recursively update the availability for all evse/connectors
12+
response = availability.NewChangeAvailabilityResponse(availability.ChangeAvailabilityStatusAccepted)
13+
return
14+
}
15+
reqEvse := request.Evse
16+
if e, ok := handler.evse[reqEvse.ID]; ok {
17+
// Changing availability for a specific EVSE
18+
if reqEvse.ConnectorID != nil {
19+
// Changing availability for a specific connector
20+
if !e.hasConnector(*reqEvse.ConnectorID) {
21+
response = availability.NewChangeAvailabilityResponse(availability.ChangeAvailabilityStatusRejected)
22+
} else {
23+
connector := e.connectors[*reqEvse.ConnectorID]
24+
connector.availability = request.OperationalStatus
25+
e.connectors[*reqEvse.ConnectorID] = connector
26+
response = availability.NewChangeAvailabilityResponse(availability.ChangeAvailabilityStatusAccepted)
27+
}
28+
return
29+
}
30+
e.availability = request.OperationalStatus
31+
response = availability.NewChangeAvailabilityResponse(availability.ChangeAvailabilityStatusAccepted)
32+
return
33+
}
34+
// No EVSE with such ID found
35+
response = availability.NewChangeAvailabilityResponse(availability.ChangeAvailabilityStatusRejected)
36+
return
37+
}
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
package main
2+
3+
import (
4+
"crypto/tls"
5+
"crypto/x509"
6+
"os"
7+
"strconv"
8+
"time"
9+
10+
"github.com/lorenzodonini/ocpp-go/ocpp2.0.1"
11+
"github.com/lorenzodonini/ocpp-go/ocpp2.0.1/availability"
12+
"github.com/lorenzodonini/ocpp-go/ocpp2.0.1/localauth"
13+
"github.com/lorenzodonini/ocpp-go/ocpp2.0.1/provisioning"
14+
"github.com/lorenzodonini/ocpp-go/ocpp2.0.1/reservation"
15+
"github.com/lorenzodonini/ocpp-go/ocpp2.0.1/transactions"
16+
"github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types"
17+
18+
"github.com/sirupsen/logrus"
19+
20+
"github.com/lorenzodonini/ocpp-go/ocppj"
21+
"github.com/lorenzodonini/ocpp-go/ws"
22+
)
23+
24+
const (
25+
envVarClientID = "CLIENT_ID"
26+
envVarCSMSUrl = "CSMS_URL"
27+
envVarTls = "TLS_ENABLED"
28+
envVarCACertificate = "CA_CERTIFICATE_PATH"
29+
envVarClientCertificate = "CLIENT_CERTIFICATE_PATH"
30+
envVarClientCertificateKey = "CLIENT_CERTIFICATE_KEY_PATH"
31+
)
32+
33+
var log *logrus.Logger
34+
35+
func setupChargingStation(chargingStationID string) ocpp2.ChargingStation {
36+
return ocpp2.NewChargingStation(chargingStationID, nil, nil)
37+
}
38+
39+
func setupTlsChargingStation(chargingStationID string) ocpp2.ChargingStation {
40+
certPool, err := x509.SystemCertPool()
41+
if err != nil {
42+
log.Fatal(err)
43+
}
44+
// Load CA cert
45+
caPath, ok := os.LookupEnv(envVarCACertificate)
46+
if ok {
47+
caCert, err := os.ReadFile(caPath)
48+
if err != nil {
49+
log.Warn(err)
50+
} else if !certPool.AppendCertsFromPEM(caCert) {
51+
log.Info("no ca.cert file found, will use system CA certificates")
52+
}
53+
} else {
54+
log.Info("no ca.cert file found, will use system CA certificates")
55+
}
56+
// Load client certificate
57+
clientCertPath, ok1 := os.LookupEnv(envVarClientCertificate)
58+
clientKeyPath, ok2 := os.LookupEnv(envVarClientCertificateKey)
59+
var clientCertificates []tls.Certificate
60+
if ok1 && ok2 {
61+
certificate, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath)
62+
if err == nil {
63+
clientCertificates = []tls.Certificate{certificate}
64+
} else {
65+
log.Infof("couldn't load client TLS certificate: %v", err)
66+
}
67+
}
68+
// Create client with TLS config
69+
client := ws.NewClient(ws.WithClientTLSConfig(&tls.Config{
70+
RootCAs: certPool,
71+
Certificates: clientCertificates,
72+
}))
73+
return ocpp2.NewChargingStation(chargingStationID, nil, client)
74+
}
75+
76+
// exampleRoutine simulates a charging station flow, where a dummy transaction is started.
77+
// The simulation runs for about 5 minutes.
78+
func exampleRoutine(chargingStation ocpp2.ChargingStation, stateHandler *ChargingStationHandler) {
79+
dummyClientIdToken := types.IdToken{
80+
IdToken: "12345",
81+
Type: types.IdTokenTypeKeyCode,
82+
}
83+
// Boot
84+
bootResp, err := chargingStation.BootNotification(provisioning.BootReasonPowerUp, "model1", "vendor1")
85+
checkError(err)
86+
logDefault(bootResp.GetFeatureName()).Infof("status: %v, interval: %v, current time: %v", bootResp.Status, bootResp.Interval, bootResp.CurrentTime.String())
87+
// Notify EVSE status
88+
for eID, e := range stateHandler.evse {
89+
updateOperationalStatus(stateHandler, eID, availability.OperationalStatusOperative)
90+
// Notify connector status
91+
for cID := range e.connectors {
92+
updateConnectorStatus(stateHandler, eID, cID, availability.ConnectorStatusAvailable)
93+
}
94+
}
95+
// Wait for some time ...
96+
time.Sleep(5 * time.Second)
97+
// Simulate charging for connector 1
98+
// EV is plugged in
99+
evseID := 1
100+
evse := stateHandler.evse[evseID]
101+
chargingConnector := 0
102+
updateConnectorStatus(stateHandler, evseID, chargingConnector, availability.ConnectorStatusOccupied)
103+
// Start transaction
104+
tx := transactions.Transaction{
105+
TransactionID: pseudoUUID(),
106+
ChargingState: transactions.ChargingStateEVConnected,
107+
}
108+
evseReq := types.EVSE{ID: evseID, ConnectorID: &chargingConnector}
109+
txEventResp, err := chargingStation.TransactionEvent(transactions.TransactionEventStarted, types.Now(), transactions.TriggerReasonCablePluggedIn, evse.nextSequence(), tx, func(request *transactions.TransactionEventRequest) {
110+
request.Evse = &evseReq
111+
})
112+
checkError(err)
113+
logDefault(txEventResp.GetFeatureName()).Infof("transaction %v started", tx.TransactionID)
114+
stateHandler.evse[evseID].currentTransaction = tx.TransactionID
115+
// Authorize
116+
authResp, err := chargingStation.Authorize(dummyClientIdToken.IdToken, types.IdTokenTypeKeyCode)
117+
checkError(err)
118+
logDefault(authResp.GetFeatureName()).Infof("status: %v %v", authResp.IdTokenInfo.Status, getExpiryDate(&authResp.IdTokenInfo))
119+
// Update transaction with auth info
120+
txEventResp, err = chargingStation.TransactionEvent(transactions.TransactionEventUpdated, types.Now(), transactions.TriggerReasonAuthorized, evse.nextSequence(), tx, func(request *transactions.TransactionEventRequest) {
121+
request.Evse = &evseReq
122+
request.IDToken = &dummyClientIdToken
123+
})
124+
checkError(err)
125+
logDefault(txEventResp.GetFeatureName()).Infof("transaction %v updated", tx.TransactionID)
126+
// Update transaction after energy offering starts
127+
txEventResp, err = chargingStation.TransactionEvent(transactions.TransactionEventUpdated, types.Now(), transactions.TriggerReasonChargingStateChanged, evse.nextSequence(), tx, func(request *transactions.TransactionEventRequest) {
128+
request.Evse = &evseReq
129+
request.IDToken = &dummyClientIdToken
130+
})
131+
checkError(err)
132+
logDefault(txEventResp.GetFeatureName()).Infof("transaction %v updated", tx.TransactionID)
133+
// Periodically send meter values
134+
var sampleInterval time.Duration = 5
135+
//sampleInterval, ok := stateHandler.configuration.getInt(MeterValueSampleInterval)
136+
//if !ok {
137+
// sampleInterval = 5
138+
//}
139+
var sampledValue types.SampledValue
140+
for i := 0; i < 5; i++ {
141+
time.Sleep(time.Second * sampleInterval)
142+
stateHandler.meterValue += 10
143+
sampledValue = types.SampledValue{
144+
Value: stateHandler.meterValue,
145+
Context: types.ReadingContextSamplePeriodic,
146+
Measurand: types.MeasurandEnergyActiveExportRegister,
147+
Phase: types.PhaseL3,
148+
Location: types.LocationOutlet,
149+
UnitOfMeasure: &types.UnitOfMeasure{
150+
Unit: "kWh",
151+
},
152+
}
153+
meterValue := types.MeterValue{
154+
Timestamp: types.DateTime{Time: time.Now()},
155+
SampledValue: []types.SampledValue{sampledValue},
156+
}
157+
// Send meter values
158+
txEventResp, err = chargingStation.TransactionEvent(transactions.TransactionEventUpdated, types.Now(), transactions.TriggerReasonMeterValuePeriodic, evse.nextSequence(), tx, func(request *transactions.TransactionEventRequest) {
159+
request.MeterValue = []types.MeterValue{meterValue}
160+
request.IDToken = &dummyClientIdToken
161+
})
162+
checkError(err)
163+
logDefault(txEventResp.GetFeatureName()).Infof("transaction %v updated with periodic meter values", tx.TransactionID)
164+
// Increase meter value
165+
stateHandler.meterValue += 2
166+
}
167+
// Stop charging for connector 1
168+
updateConnectorStatus(stateHandler, evseID, chargingConnector, availability.ConnectorStatusAvailable)
169+
// Send transaction end data
170+
sampledValue.Context = types.ReadingContextTransactionEnd
171+
sampledValue.Value = stateHandler.meterValue
172+
tx.StoppedReason = transactions.ReasonEVDisconnected
173+
txEventResp, err = chargingStation.TransactionEvent(transactions.TransactionEventEnded, types.Now(), transactions.TriggerReasonEVCommunicationLost, evse.nextSequence(), tx, func(request *transactions.TransactionEventRequest) {
174+
request.Evse = &evseReq
175+
request.IDToken = &dummyClientIdToken
176+
request.MeterValue = []types.MeterValue{}
177+
})
178+
checkError(err)
179+
logDefault(txEventResp.GetFeatureName()).Infof("transaction %v stopped", tx.TransactionID)
180+
// Wait for some time ...
181+
time.Sleep(5 * time.Minute)
182+
// End simulation
183+
}
184+
185+
// Start function
186+
func main() {
187+
// Load config
188+
id, ok := os.LookupEnv(envVarClientID)
189+
if !ok {
190+
log.Printf("no %v environment variable found, exiting...", envVarClientID)
191+
return
192+
}
193+
csmsUrl, ok := os.LookupEnv(envVarCSMSUrl)
194+
if !ok {
195+
log.Printf("no %v environment variable found, exiting...", envVarCSMSUrl)
196+
return
197+
}
198+
// Check if TLS enabled
199+
t, _ := os.LookupEnv(envVarTls)
200+
tlsEnabled, _ := strconv.ParseBool(t)
201+
// Prepare OCPP 2.0.1 charging station (chargingStation variable is defined in handler.go)
202+
if tlsEnabled {
203+
chargingStation = setupTlsChargingStation(id)
204+
} else {
205+
chargingStation = setupChargingStation(id)
206+
}
207+
// Setup some basic state management
208+
evse := EVSEInfo{
209+
availability: availability.OperationalStatusOperative,
210+
currentTransaction: "",
211+
currentReservation: 0,
212+
connectors: map[int]ConnectorInfo{
213+
0: {
214+
status: availability.ConnectorStatusAvailable,
215+
availability: availability.OperationalStatusOperative,
216+
typ: reservation.ConnectorTypeCType2,
217+
},
218+
},
219+
seqNo: 0,
220+
}
221+
handler := &ChargingStationHandler{
222+
model: "model1",
223+
vendor: "vendor1",
224+
availability: availability.OperationalStatusOperative,
225+
evse: map[int]*EVSEInfo{1: &evse},
226+
localAuthList: []localauth.AuthorizationData{},
227+
localAuthListVersion: 0,
228+
monitoringLevel: 0,
229+
meterValue: 0,
230+
}
231+
// Support callbacks for all OCPP 2.0.1 profiles
232+
chargingStation.SetAvailabilityHandler(handler)
233+
chargingStation.SetAuthorizationHandler(handler)
234+
chargingStation.SetDataHandler(handler)
235+
chargingStation.SetDiagnosticsHandler(handler)
236+
chargingStation.SetDisplayHandler(handler)
237+
chargingStation.SetFirmwareHandler(handler)
238+
chargingStation.SetISO15118Handler(handler)
239+
chargingStation.SetLocalAuthListHandler(handler)
240+
chargingStation.SetProvisioningHandler(handler)
241+
chargingStation.SetRemoteControlHandler(handler)
242+
chargingStation.SetReservationHandler(handler)
243+
chargingStation.SetSmartChargingHandler(handler)
244+
chargingStation.SetTariffCostHandler(handler)
245+
chargingStation.SetTransactionsHandler(handler)
246+
ocppj.SetLogger(log)
247+
// Connects to central system
248+
err := chargingStation.Start(csmsUrl)
249+
if err != nil {
250+
log.Error(err)
251+
} else {
252+
log.Infof("connected to CSMS at %v", csmsUrl)
253+
exampleRoutine(chargingStation, handler)
254+
// Disconnect
255+
chargingStation.Stop()
256+
log.Infof("disconnected from CSMS")
257+
}
258+
}
259+
260+
func init() {
261+
log = logrus.New()
262+
log.SetFormatter(&logrus.TextFormatter{FullTimestamp: true})
263+
log.SetLevel(logrus.InfoLevel)
264+
}
265+
266+
// Utility functions
267+
func logDefault(feature string) *logrus.Entry {
268+
return log.WithField("message", feature)
269+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package main
2+
3+
import (
4+
"crypto/rand"
5+
"fmt"
6+
)
7+
8+
// Generates a pseudo UUID. Not RFC 4122 compliant, but useful for this example.
9+
func pseudoUUID() (uuid string) {
10+
b := make([]byte, 16)
11+
_, err := rand.Read(b)
12+
if err != nil {
13+
fmt.Println("Error: ", err)
14+
return
15+
}
16+
uuid = fmt.Sprintf("%X-%X-%X-%X-%X", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
17+
return
18+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"github.com/lorenzodonini/ocpp-go/ocpp2.0.1/data"
6+
)
7+
8+
type DataSample struct {
9+
SampleString string `json:"sample_string"`
10+
SampleValue float64 `json:"sample_value"`
11+
}
12+
13+
func (handler *ChargingStationHandler) OnDataTransfer(request *data.DataTransferRequest) (response *data.DataTransferResponse, err error) {
14+
var dataSample DataSample
15+
err = json.Unmarshal(request.Data.([]byte), &dataSample)
16+
if err != nil {
17+
logDefault(request.GetFeatureName()).
18+
Errorf("invalid data received: %v", request.Data)
19+
return nil, err
20+
}
21+
logDefault(request.GetFeatureName()).
22+
Infof("data received: %v, %v", dataSample.SampleString, dataSample.SampleValue)
23+
return data.NewDataTransferResponse(data.DataTransferStatusAccepted), nil
24+
}

0 commit comments

Comments
 (0)