This simulator supports:
- OCPP 1.6 (and security) with JSON
- REST API with HTML Frontend
- File based batch mode
- Fully scriptable in JavaScript
- ftp and csr operations
- ws:// and wss:// (with client certificates)
Key development considerations:
- This tool should help you in learning and understanding the Open Charge Point Protocol (OCPP), therefore it doesn't hide any details, but make them easier to grasp
- It's an all ECMAScript based project
- NodeJs based server with server-side TypeScript support
- Vue + Vuex + bulma + axios + vue-axios based HTML client with super simple requirejs no-transpiler setup
- TODO: OCPP code should run inside a browser too
This software is licensed under Apache License, Version 2.0. See LICENSE.
Make sure you've npm installed.
Put the logic using JavaScript into a file (e.g. custom.js). Your file needs to export an async function with one
parameter. This parameter will pass the connect(url: string): Chargepoint
function to obtain a Chargepoint class object.
Example:
let cp;
try {
// WebSocket Connect (no OCPP)
cp = await connect('ws://localhost:8100/xyz');
// typical startup OCPP
await cp.sendBootnotification({chargePointVendor: "vendor", chargePointModel: "1"});
await cp.sendHeartbeat();
await cp.sendStatusNotification({connectorId: 0, errorCode: "NoError", status: "Available"});
await cp.sendStatusNotification({connectorId: 1, errorCode: "NoError", status: "Available"});
// register code for GetDiagnostics, UpdateFirmware, Reset, ...
cp.answerGetDiagnostics( async (request) => {
const fileName = "foo." + new Date().toISOString() + ".txt";
cp.sendResponse(request.uniqueId, {fileName});
await cp.sendDiagnosticsStatusNotification({status: "Idle"});
await cp.sleep(5000);
await cp.sendDiagnosticsStatusNotification({status: "Uploading"});
await cp.ftpUploadDummyFile(request.payload.location, fileName);
await cp.sendDiagnosticsStatusNotification({status: "Uploaded"});
});
// Typical charging session
await cp.sendAuthorize({idTag: "ccc"});
await cp.sendStatusNotification({connectorId: 1, errorCode: "NoError", status: "Preparing"});
cp.transaction = await cp.startTransaction({connectorId: 1, idTag: "ccc", meterStart: 1377, timestamp: "2020-06-11T10:50:58.333Z"});
await cp.sendStatusNotification({connectorId: 1, errorCode: "NoError", status: "Charging"});
await cp.meterValues({connectorId: 1, transactionId: cp.transaction.transactionId, meterValue: [{ timestamp: "2020-06-11T10:50:58.765Z", sampledValue: [{value: 1387}] }]});
await cp.stopTransaction({transactionId: cp.transaction.transactionId, meterStop: 1399, timestamp: "2020-06-11T10:50:59.148Z"});
await cp.sendStatusNotification({connectorId: 1, errorCode: "NoError", status: "Finishing"});
await cp.sendStatusNotification({connectorId: 1, errorCode: "NoError", status: "Available"});
} catch (err) {
console.log(err);
} finally {
cp.close();
}
Start it:
./start.sh --v ./custom.js
# or
cat custom.js | ./start.sh --v --stdin
You can start a web server within the batch operation script to allow the manipulation or observation of the script.
const webserver = cp.startListener(8080, '0.0.0.0', {'admin': 'secret'});
webserver.get('/stop', (req, res) => {
res.send('stopped.');
webserver.terminate();
});
webserver.get('/availability', (req, res) => {
// this assumes we're storing the current availability in a variable called 'availability'
res.send(availability);
});
This example starts a web server on 0.0.0.0:8080 using basic authentication. A user 'admin' with the password 'secret' allows under /stop to stop the webserver and under /availability to retrieve the charge points availability.
Default port for HTML is 3000. Change via env variable PORT
. The WebSocket based Server to Client communication is using PORT+1
.
./start.sh --v
Open http://localhost:3000/?connectTemplate=$connectUrl&cp=$chargePointName
where chargePointName defines the ID of your chargepoint and connectUrl the connect string without the chargepoint-id at the end.
Example: http://localhost:3000/?connectTemplate=ws://foobar:8088/charging&cp=chargepoint001
.
npm run docs
Open the docs in ./public/docs or access them via ./start.sh
and http://localhost:3000/docs
To pass a root CA file (to verify the server's certificate) use the --ca parameter from start.sh or set the env variable SSL_CERT_FILE.
To set client certificates (for mTLS) for a charge point with the id my-chargepoint-id
use the following parameters:
./start.sh --v1 --keyStore '[{"id": "my-chargepoint-id", "key": "private.pem", "cert": "cert.pem"}]' --ca ./ca.pem
Run:
./start.sh --v1 --d
The simulator will respond to all Trigger Message
with status=NotImplemented
if no answerTriggerMessage
have been
registered for this requestedMessage
.
OCPP 1.6 defines those requestedMessage:
- "BootNotification"
- "DiagnosticsStatusNotification"
- "FirmwareStatusNotification"
- "Heartbeat"
- "MeterValues"
- "StatusNotification"
Example for BootNotification
cp.answerTriggerMessage("BootNotification", async (request) => {
cp.sendResponse(request.uniqueId, {status: "Accepted"});
await cp.sendBootnotification({chargePointVendor: "vendor", chargePointModel: "1"});
});
Another example for DiagnosticsStatusNotification
// your code for handling GetDiagnostics will need to update a variable
// currentDiagnosticsStatus with the current state
cp.answerTriggerMessage("DiagnosticsStatusNotification", async (request) => {
if(currentDiagnosticsStatus) {
cp.sendResponse(request.uniqueId, {status: "Accepted"});
await cp.sendDiagnosticsStatusNotification({status: currentDiagnosticsStatus});
} else {
cp.sendResponse(request.uniqueId, {status: "Rejected"});
}
});
OCPP 1.6 security defines those requestedMessage:
- "BootNotification"
- "LogStatusNotification"
- "FirmwareStatusNotification"
- "Heartbeat"
- "MeterValues"
- "SignChargePointCertificate"
- "StatusNotification"
Example of SignChargePointCertificate:
let tmpKey;
cp.answerExtendedTriggerMessage("SignChargePointCertificate", async (request) => {
cp.sendResponse(request.uniqueId, {status: "Accepted"});
const {key, csr} = await cp.generateCsr('/OU=Ocpp-Simulator/O=Ocpp Simu Inc./L=Paradise City/ST=The State/C=US/CN=the-best-chargepoint');
tmpKey = key;
await cp.sendSignCertificate({csr, "typeOfCertificate": "ChargingStationCertificate"});
});
cp.answerCertificateSigned( async (request) => {
if(!tmpKey) {
cp.sendResponse(request.uniqueId, {status: "Rejected"});
return;
}
cp.sendResponse(request.uniqueId, {status: "Accepted"});
const keystore = cp.keystore();
// this will overwrite the current key/cert files
const filenames = keystore.save(false, tmpKey, request.payload.cert.join('\n'));
// alternatively certs/key can be written in a new file
// keystore.save('-' + new Date().toISOString(), tmpKey, request.payload.cert.join('\n'));
await cp.reConnect();
await cp.sendBootnotification({chargePointVendor: "vendor", chargePointModel: "1"});
}, cp.CERTIFICATE_SIGNED_OPTIONS_PEM_ENCODER());
- BootNotification
- HeartBeat
- StatusNotification
- Authorize
- StartTransaction
- StopTransaction
- MeterValues
- Get Diagnostics
- Diagnostics Status Notification
- Update Firmware
- Firmware Status Notification
- Trigger Message
- Reset
- Get Configuration
- Change Configuration
- Change Availability
- Remote Start Transaction
- ExtendedTriggerMessage (1.6 security)
- SignCertificate (1.6 security)
- CertificateSigned (1.6 security)
- Cancel Reservation
- Clear Cache
- Clear Charging Profile
- Data Transfer
- Get Composite Schedule
- Get Local List Version
- Remote Stop Transaction
- Reserve Now
- Send Local List
- Set Charging Profile
- Unlock Connector
To disable the host name verification when using TLS, apply this patch:
diff --git a/src/websocket-connection-centralsystem.ts b/src/websocket-connection-centralsystem.ts
index ec53fdc..e0323a8 100644
--- a/src/websocket-connection-centralsystem.ts
+++ b/src/websocket-connection-centralsystem.ts
@@ -32,6 +32,7 @@ export class WSConCentralSystem{
options.key = fs.readFileSync(keyStoreElement.key);
options.cert = fs.readFileSync(keyStoreElement.cert);
}
+ options.checkServerIdentity = () => undefined;
}
this.ws = new WebSocket(this.url, "ocpp1.6", options);
let promiseResolved = false;