## Summary
-Simple [node.js](https://nodejs.org/) program to simulate a set of charging stations based on the OCPP-J 1.6 protocol.
+Simple [node.js](https://nodejs.org/) software to simulate a set of charging stations based on the OCPP-J 1.6 protocol as part of SAP e-Mobility solution.
## Prerequisites
**src/assets/station-templates/\<name\>.json**:
-| Key | Value(s) | Default Value | Value type | Description |
-| --------------------------------- | ---------- | --------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| supervisionUrls | | '' | string \| string[] | string or array of connection URIs to OCPP-J servers |
-| supervisionUser | | '' | string | basic HTTP authentication user to OCPP-J server |
-| supervisionPassword | | '' | string | basic HTTP authentication password to OCPP-J server |
-| supervisionUrlOcppConfiguration | true/false | false | boolean | allow supervision URL configuration via a vendor OCPP parameter key |
-| supervisionUrlOcppKey | | 'ConnectionUrl' | string | the vendor string that will be used as a vendor OCPP parameter key to set the supervision URL |
-| ocppVersion | 1.6 | 1.6 | string | OCPP version |
-| ocppProtocol | json | json | string | OCPP protocol |
-| ocppStrictCompliance | true/false | false | boolean | strict adherence to the OCPP version and protocol specifications |
-| ocppPersistentConfiguration | true/false | true | boolean | enable persistent OCPP parameters storage by charging stations 'hashId'. The persistency is ensured by the charging stations configuration files in dist/assets/configurations |
-| wsOptions | | {} | ClientOptions & ClientRequestArgs | [ws](https://github.com/websockets/ws) and node.js [http](https://nodejs.org/api/http.html) clients options intersection |
-| authorizationFile | | '' | string | RFID tags list file relative to src/assets path |
-| baseName | | '' | string | base name to build charging stations id |
-| nameSuffix | | '' | string | name suffix to build charging stations id |
-| fixedName | true/false | false | boolean | use the baseName as the charging stations unique name |
-| chargePointModel | | '' | string | charging stations model |
-| chargePointVendor | | '' | string | charging stations vendor |
-| chargePointSerialNumberPrefix | | '' | string | charge point serial number prefix |
-| chargeBoxSerialNumberPrefix | | '' | string | charge box serial number prefix (deprecated in OCPP 1.6) |
-| firmwareVersion | | '' | string | charging stations firmware version |
-| power | | | float \| float[] | charging stations maximum power value(s) |
-| powerSharedByConnectors | true/false | false | boolean | charging stations power shared by its connectors |
-| powerUnit | W/kW | W | string | charging stations power unit |
-| currentOutType | AC/DC | AC | string | charging stations current out type |
-| voltageOut | | AC:230/DC:400 | integer | charging stations voltage out |
-| numberOfPhases | 0/1/3 | AC:3/DC:0 | integer | charging stations number of phase(s) |
-| numberOfConnectors | | | integer \| integer[] | charging stations number of connector(s) |
-| useConnectorId0 | true/false | true | boolean | use connector id 0 definition from the charging station configuration template |
-| randomConnectors | true/false | false | boolean | randomize runtime connector id affectation from the connector id definition in charging station configuration template |
-| resetTime | | 60 | integer | seconds to wait before the charging stations come back at reset |
-| autoRegister | true/false | false | boolean | set charging stations as registered at boot notification for testing purpose |
-| autoReconnectMaxRetries | | -1 (unlimited) | integer | connection retries to the OCPP-J server |
-| reconnectExponentialDelay | true/false | false | boolean | connection delay retry to the OCPP-J server |
-| registrationMaxRetries | | -1 (unlimited) | integer | charging stations boot notification retries |
-| amperageLimitationOcppKey | | undefined | string | charging stations OCPP parameter key used to set the amperage limit, per phase for each connector on AC and global for DC |
-| amperageLimitationUnit | A/cA/dA/mA | A | string | charging stations amperage limit unit |
-| enableStatistics | true/false | true | boolean | enable charging stations statistics |
-| mayAuthorizeAtRemoteStart | true/false | true | boolean | always send authorize at remote start transaction when AuthorizeRemoteTxRequests is enabled |
-| beginEndMeterValues | true/false | false | boolean | enable Transaction.{Begin,End} MeterValues |
-| outOfOrderEndMeterValues | true/false | false | boolean | send Transaction.End MeterValues out of order. Need to relax OCPP specifications strict compliance ('ocppStrictCompliance' parameter) |
-| meteringPerTransaction | true/false | true | boolean | enable metering history on a per transaction basis |
-| transactionDataMeterValues | true/false | false | boolean | enable transaction data MeterValues at stop transaction |
-| mainVoltageMeterValues | true/false | true | boolean | include charging stations main voltage MeterValues on three phased charging stations |
-| phaseLineToLineVoltageMeterValues | true/false | true | boolean | include charging stations line to line voltage MeterValues on three phased charging stations |
-| customValueLimitationMeterValues | true/false | true | boolean | enable limitation on custom fluctuated value in MeterValues |
-| Configuration | | | ChargingStationConfiguration | charging stations OCPP parameters configuration section |
-| AutomaticTransactionGenerator | | | AutomaticTransactionGenerator | charging stations ATG configuration section |
-| Connectors | | | Connectors | charging stations connectors configuration section |
+| Key | Value(s) | Default Value | Value type | Description |
+| ---------------------------------- | ---------- | --------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| supervisionUrls | | '' | string \| string[] | string or array of connection URIs to OCPP-J servers |
+| supervisionUser | | '' | string | basic HTTP authentication user to OCPP-J server |
+| supervisionPassword | | '' | string | basic HTTP authentication password to OCPP-J server |
+| supervisionUrlOcppConfiguration | true/false | false | boolean | allow supervision URL configuration via a vendor OCPP parameter key |
+| supervisionUrlOcppKey | | 'ConnectionUrl' | string | the vendor string that will be used as a vendor OCPP parameter key to set the supervision URL |
+| ocppVersion | 1.6 | 1.6 | string | OCPP version |
+| ocppProtocol | json | json | string | OCPP protocol |
+| ocppStrictCompliance | true/false | false | boolean | strict adherence to the OCPP version and protocol specifications |
+| ocppPersistentConfiguration | true/false | true | boolean | enable persistent OCPP parameters storage by charging stations 'hashId'. The persistency is ensured by the charging stations configuration files in dist/assets/configurations |
+| stationInfoPersistentConfiguration | true/false | true | boolean | enable persistent station information and specifications storage by charging stations 'hashId'. The persistency is ensured by the charging stations configuration files in dist/assets/configurations |
+| wsOptions | | {} | ClientOptions & ClientRequestArgs | [ws](https://github.com/websockets/ws) and node.js [http](https://nodejs.org/api/http.html) clients options intersection |
+| authorizationFile | | '' | string | RFID tags list file relative to src/assets path |
+| baseName | | '' | string | base name to build charging stations id |
+| nameSuffix | | '' | string | name suffix to build charging stations id |
+| fixedName | true/false | false | boolean | use the baseName as the charging stations unique name |
+| chargePointModel | | '' | string | charging stations model |
+| chargePointVendor | | '' | string | charging stations vendor |
+| chargePointSerialNumberPrefix | | '' | string | charge point serial number prefix |
+| chargeBoxSerialNumberPrefix | | '' | string | charge box serial number prefix (deprecated in OCPP 1.6) |
+| firmwareVersion | | '' | string | charging stations firmware version |
+| power | | | float \| float[] | charging stations maximum power value(s) |
+| powerSharedByConnectors | true/false | false | boolean | charging stations power shared by its connectors |
+| powerUnit | W/kW | W | string | charging stations power unit |
+| currentOutType | AC/DC | AC | string | charging stations current out type |
+| voltageOut | | AC:230/DC:400 | integer | charging stations voltage out |
+| numberOfPhases | 0/1/3 | AC:3/DC:0 | integer | charging stations number of phase(s) |
+| numberOfConnectors | | | integer \| integer[] | charging stations number of connector(s) |
+| useConnectorId0 | true/false | true | boolean | use connector id 0 definition from the charging station configuration template |
+| randomConnectors | true/false | false | boolean | randomize runtime connector id affectation from the connector id definition in charging station configuration template |
+| resetTime | | 60 | integer | seconds to wait before the charging stations come back at reset |
+| autoRegister | true/false | false | boolean | set charging stations as registered at boot notification for testing purpose |
+| autoReconnectMaxRetries | | -1 (unlimited) | integer | connection retries to the OCPP-J server |
+| reconnectExponentialDelay | true/false | false | boolean | connection delay retry to the OCPP-J server |
+| registrationMaxRetries | | -1 (unlimited) | integer | charging stations boot notification retries |
+| amperageLimitationOcppKey | | undefined | string | charging stations OCPP parameter key used to set the amperage limit, per phase for each connector on AC and global for DC |
+| amperageLimitationUnit | A/cA/dA/mA | A | string | charging stations amperage limit unit |
+| enableStatistics | true/false | true | boolean | enable charging stations statistics |
+| mayAuthorizeAtRemoteStart | true/false | true | boolean | always send authorize at remote start transaction when AuthorizeRemoteTxRequests is enabled |
+| beginEndMeterValues | true/false | false | boolean | enable Transaction.{Begin,End} MeterValues |
+| outOfOrderEndMeterValues | true/false | false | boolean | send Transaction.End MeterValues out of order. Need to relax OCPP specifications strict compliance ('ocppStrictCompliance' parameter) |
+| meteringPerTransaction | true/false | true | boolean | enable metering history on a per transaction basis |
+| transactionDataMeterValues | true/false | false | boolean | enable transaction data MeterValues at stop transaction |
+| mainVoltageMeterValues | true/false | true | boolean | include charging stations main voltage MeterValues on three phased charging stations |
+| phaseLineToLineVoltageMeterValues | true/false | true | boolean | include charging stations line to line voltage MeterValues on three phased charging stations |
+| customValueLimitationMeterValues | true/false | true | boolean | enable limitation on custom fluctuated value in MeterValues |
+| Configuration | | | ChargingStationConfiguration | charging stations OCPP parameters configuration section |
+| AutomaticTransactionGenerator | | | AutomaticTransactionGenerator | charging stations ATG configuration section |
+| Connectors | | | Connectors | charging stations connectors configuration section |
#### Configuration section
let startResponse: StartTransactionResponse;
if (this.chargingStation.hasAuthorizedTags()) {
const idTag = this.chargingStation.getRandomIdTag();
- if (this.chargingStation.getAutomaticTransactionGeneratorRequireAuthorize()) {
+ if (this.getRequireAuthorize()) {
this.chargingStation.getConnectorStatus(connectorId).authorizeIdTag = idTag;
// Authorize idTag
const authorizeResponse: AuthorizeResponse =
return stopResponse;
}
+ private getRequireAuthorize(): boolean {
+ return (
+ this.chargingStation.stationInfo?.AutomaticTransactionGenerator?.requireAuthorize ?? true
+ );
+ }
+
private logPrefix(connectorId?: number): string {
if (connectorId) {
return Utils.logPrefix(
}
public logPrefix(): string {
- return Utils.logPrefix(` ${this.stationInfo.chargingStationId} |`);
+ return Utils.logPrefix(
+ ` ${
+ this?.stationInfo?.chargingStationId ??
+ ChargingStationUtils.getChargingStationId(this.index, this.getTemplateFromFile())
+ } |`
+ );
}
public getBootNotificationRequest(): BootNotificationRequest {
}
public getOcppStrictCompliance(): boolean {
- return this.stationInfo.ocppStrictCompliance ?? false;
+ return this.stationInfo?.ocppStrictCompliance ?? false;
}
public getVoltageOut(): number | undefined {
}
public getOutOfOrderEndMeterValues(): boolean {
- return this.stationInfo.outOfOrderEndMeterValues ?? false;
+ return this.stationInfo?.outOfOrderEndMeterValues ?? false;
}
public getBeginEndMeterValues(): boolean {
- return this.stationInfo.beginEndMeterValues ?? false;
+ return this.stationInfo?.beginEndMeterValues ?? false;
}
public getMeteringPerTransaction(): boolean {
- return this.stationInfo.meteringPerTransaction ?? true;
+ return this.stationInfo?.meteringPerTransaction ?? true;
}
public getTransactionDataMeterValues(): boolean {
- return this.stationInfo.transactionDataMeterValues ?? false;
+ return this.stationInfo?.transactionDataMeterValues ?? false;
}
public getMainVoltageMeterValues(): boolean {
- return this.stationInfo.mainVoltageMeterValues ?? true;
+ return this.stationInfo?.mainVoltageMeterValues ?? true;
}
public getPhaseLineToLineVoltageMeterValues(): boolean {
- return this.stationInfo.phaseLineToLineVoltageMeterValues ?? false;
+ return this.stationInfo?.phaseLineToLineVoltageMeterValues ?? false;
}
public getCustomValueLimitationMeterValues(): boolean {
- return this.stationInfo.customValueLimitationMeterValues ?? true;
+ return this.stationInfo?.customValueLimitationMeterValues ?? true;
}
public getConnectorIdByTransactionId(transactionId: number): number | undefined {
return localAuthListEnabled ? Utils.convertToBoolean(localAuthListEnabled.value) : false;
}
- public getAutomaticTransactionGeneratorRequireAuthorize(): boolean {
- return this.stationInfo.AutomaticTransactionGenerator.requireAuthorize ?? true;
- }
-
public startHeartbeat(): void {
if (
this.getHeartbeatInterval() &&
try {
const measureId = `${FileType.ChargingStationTemplate} read`;
const beginId = PerformanceStatistics.beginMeasure(measureId);
- template =
- (JSON.parse(fs.readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate) ??
- ({} as ChargingStationTemplate);
+ template = JSON.parse(fs.readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate;
PerformanceStatistics.endMeasure(measureId, beginId);
template.templateHash = crypto
.createHash(Constants.DEFAULT_HASH_ALGORITHM)
private getStationInfoFromTemplate(): ChargingStationInfo {
const stationInfo: ChargingStationInfo = this.getTemplateFromFile();
if (Utils.isNullOrUndefined(stationInfo)) {
- const logMsg = 'Failed to read charging station template file';
- logger.error(`${this.logPrefix()} ${logMsg}`);
- throw new BaseError(logMsg);
+ const errorMsg = 'Failed to read charging station template file';
+ logger.error(`${this.logPrefix()} ${errorMsg}`);
+ throw new BaseError(errorMsg);
}
if (Utils.isEmptyObject(stationInfo)) {
- logger.warn(
- `${this.logPrefix()} Empty charging station information from template file ${
- this.templateFile
- }`
- );
+ const errorMsg = `Empty charging station information from template file ${this.templateFile}`;
+ logger.error(`${this.logPrefix()} ${errorMsg}`);
+ throw new BaseError(errorMsg);
}
const chargingStationId = ChargingStationUtils.getChargingStationId(this.index, stationInfo);
// Deprecation template keys section
stationInfo,
'supervisionUrl',
this.templateFile,
- Utils.logPrefix(` ${chargingStationId} |`),
+ this.logPrefix(),
"Use 'supervisionUrls' instead"
);
ChargingStationUtils.convertDeprecatedTemplateKey(
'supervisionUrl',
'supervisionUrls'
);
- stationInfo.wsOptions = stationInfo?.wsOptions ?? {};
if (!Utils.isEmptyArray(stationInfo.power)) {
stationInfo.power = stationInfo.power as number[];
const powerArrayRandomIndex = Math.floor(Utils.secureRandom() * stationInfo.power.length);
return stationInfo;
}
- private getStationInfoFromFile(): ChargingStationInfo {
- let stationInfo = this.getConfigurationFromFile()?.stationInfo ?? ({} as ChargingStationInfo);
- stationInfo = ChargingStationUtils.createStationInfoHash(stationInfo);
+ private getStationInfoFromFile(): ChargingStationInfo | null {
+ let stationInfo: ChargingStationInfo = null;
+ if (this.getStationInfoPersistentConfiguration()) {
+ stationInfo = this.getConfigurationFromFile()?.stationInfo ?? null;
+ }
+ if (stationInfo) {
+ stationInfo = ChargingStationUtils.createStationInfoHash(stationInfo);
+ }
return stationInfo;
}
}
private saveStationInfo(): void {
- this.saveConfiguration(Section.stationInfo);
+ if (this.getStationInfoPersistentConfiguration()) {
+ this.saveConfiguration(Section.stationInfo);
+ }
}
private getOcppVersion(): OCPPVersion {
}
private getOcppPersistentConfiguration(): boolean {
- return this.stationInfo.ocppPersistentConfiguration ?? true;
+ return this.stationInfo?.ocppPersistentConfiguration ?? true;
+ }
+
+ private getStationInfoPersistentConfiguration(): boolean {
+ return this.stationInfo?.stationInfoPersistentConfiguration ?? true;
}
private handleUnsupportedVersion(version: OCPPVersion) {
if (
maxConnectors >
(this.stationInfo?.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) &&
- !this.stationInfo.randomConnectors
+ !this.stationInfo?.randomConnectors
) {
logger.warn(
`${this.logPrefix()} Number of connectors exceeds the number of connector configurations in template ${
}
this.initializeConnectors(this.stationInfo, maxConnectors, templateMaxConnectors);
this.stationInfo.maximumAmperage = this.getMaximumAmperage();
- this.stationInfo = ChargingStationUtils.createStationInfoHash(this.stationInfo);
+ if (this.stationInfo) {
+ this.stationInfo = ChargingStationUtils.createStationInfoHash(this.stationInfo);
+ }
this.saveStationInfo();
// Avoid duplication of connectors related information in RAM
this.stationInfo?.Connectors && delete this.stationInfo.Connectors;
// Generate all connectors
if ((stationInfo?.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0) {
for (let index = 1; index <= maxConnectors; index++) {
- const randConnectorId = stationInfo.randomConnectors
+ const randConnectorId = stationInfo?.randomConnectors
? Utils.getRandomInteger(Utils.convertToInt(lastConnector), 1)
: index;
this.connectors.set(
private saveConfiguration(section?: Section): void {
if (this.configurationFile) {
try {
- const configurationData: ChargingStationConfiguration =
- this.getConfigurationFromFile() ?? {};
if (!fs.existsSync(path.dirname(this.configurationFile))) {
fs.mkdirSync(path.dirname(this.configurationFile), { recursive: true });
}
+ const configurationData: ChargingStationConfiguration =
+ this.getConfigurationFromFile() ?? {};
switch (section) {
case Section.ocppConfiguration:
configurationData.configurationKey = this.ocppConfiguration.configurationKey;
}
}
- private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration {
- return this.getTemplateFromFile().Configuration ?? ({} as ChargingStationOcppConfiguration);
+ private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | null {
+ return this.getTemplateFromFile()?.Configuration ?? null;
}
private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | null {
return configuration;
}
- private getOcppConfiguration(): ChargingStationOcppConfiguration {
+ private getOcppConfiguration(): ChargingStationOcppConfiguration | null {
let ocppConfiguration: ChargingStationOcppConfiguration = this.getOcppConfigurationFromFile();
if (!ocppConfiguration) {
ocppConfiguration = this.getOcppConfigurationFromTemplate();
}
private openWSConnection(
- options: WsOptions = this.stationInfo.wsOptions,
+ options: WsOptions = this.stationInfo?.wsOptions ?? {},
forceCloseOpened = false
): void {
options.handshakeTimeout = options?.handshakeTimeout ?? this.getConnectionTimeout() * 1000;
this.autoReconnectRetryCount.toString()
);
this.openWSConnection(
- { ...this.stationInfo.wsOptions, handshakeTimeout: reconnectTimeout },
+ { ...(this.stationInfo?.wsOptions ?? {}), handshakeTimeout: reconnectTimeout },
true
);
this.wsConnectionRestarted = true;
const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0;
const idSuffix = stationTemplate.nameSuffix ?? '';
const idStr = '000000000' + index.toString();
- return stationTemplate.fixedName
+ return stationTemplate?.fixedName
? stationTemplate.baseName
: stationTemplate.baseName +
'-' +
}
public static createStationInfoHash(stationInfo: ChargingStationInfo): ChargingStationInfo {
- if (!Utils.isEmptyObject(stationInfo)) {
- const previousInfoHash = stationInfo?.infoHash ?? '';
- delete stationInfo.infoHash;
- const currentInfoHash = crypto
- .createHash(Constants.DEFAULT_HASH_ALGORITHM)
- .update(JSON.stringify(stationInfo))
- .digest('hex');
- if (
- Utils.isEmptyString(previousInfoHash) ||
- (!Utils.isEmptyString(previousInfoHash) && currentInfoHash !== previousInfoHash)
- ) {
- stationInfo.infoHash = currentInfoHash;
- } else {
- stationInfo.infoHash = previousInfoHash;
- }
+ const previousInfoHash = stationInfo?.infoHash ?? '';
+ delete stationInfo.infoHash;
+ const currentInfoHash = crypto
+ .createHash(Constants.DEFAULT_HASH_ALGORITHM)
+ .update(JSON.stringify(stationInfo))
+ .digest('hex');
+ if (
+ Utils.isEmptyString(previousInfoHash) ||
+ (!Utils.isEmptyString(previousInfoHash) && currentInfoHash !== previousInfoHash)
+ ) {
+ stationInfo.infoHash = currentInfoHash;
+ } else {
+ stationInfo.infoHash = previousInfoHash;
}
return stationInfo;
}
public static createSerialNumber(
stationInfo: ChargingStationInfo,
- existingStationInfo?: ChargingStationInfo,
+ existingStationInfo?: ChargingStationInfo | null,
params: { randomSerialNumberUpperCase?: boolean; randomSerialNumber?: boolean } = {
randomSerialNumberUpperCase: true,
randomSerialNumber: true,
params = params ?? {};
params.randomSerialNumberUpperCase = params?.randomSerialNumberUpperCase ?? true;
params.randomSerialNumber = params?.randomSerialNumber ?? true;
- if (!Utils.isEmptyObject(existingStationInfo)) {
+ if (existingStationInfo) {
existingStationInfo?.chargePointSerialNumber &&
(stationInfo.chargePointSerialNumber = existingStationInfo.chargePointSerialNumber);
existingStationInfo?.chargeBoxSerialNumber &&
clearCurrentCP = true;
}
if (clearCurrentCP) {
- connectorStatus.chargingProfiles[index] = {} as OCPP16ChargingProfile;
+ connectorStatus.chargingProfiles.splice(index, 1);
logger.debug(
`${chargingStation.logPrefix()} Matching charging profile(s) cleared on connector id ${
commandPayload.connectorId
ocppProtocol?: OCPPProtocol;
ocppStrictCompliance?: boolean;
ocppPersistentConfiguration?: boolean;
+ stationInfoPersistentConfiguration?: boolean;
wsOptions?: WsOptions;
authorizationFile?: string;
baseName: string;