Hook the OCPP 2.0 stack into the main code
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
index 1699f046da3982353a45ec637d43adce4b91d6d8..689fe2d98099f844a42b1d15195def868f55746b 100644 (file)
@@ -1,4 +1,4 @@
-// Partial Copyright Jerome Benoit. 2021. All Rights Reserved.
+// Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
 
 import crypto from 'crypto';
 import fs from 'fs';
@@ -6,7 +6,7 @@ import path from 'path';
 import { URL } from 'url';
 import { parentPort } from 'worker_threads';
 
-import WebSocket, { Data, RawData } from 'ws';
+import WebSocket, { type RawData } from 'ws';
 
 import BaseError from '../exception/BaseError';
 import OCPPError from '../exception/OCPPError';
@@ -40,28 +40,30 @@ import { MeterValue, MeterValueMeasurand } from '../types/ocpp/MeterValues';
 import { OCPPVersion } from '../types/ocpp/OCPPVersion';
 import {
   AvailabilityType,
-  BootNotificationRequest,
-  CachedRequest,
-  HeartbeatRequest,
-  IncomingRequest,
+  type BootNotificationRequest,
+  type CachedRequest,
+  type ErrorCallback,
+  type HeartbeatRequest,
+  type IncomingRequest,
   IncomingRequestCommand,
-  MeterValuesRequest,
+  type MeterValuesRequest,
   RequestCommand,
-  StatusNotificationRequest,
+  type ResponseCallback,
+  type StatusNotificationRequest,
 } from '../types/ocpp/Requests';
 import {
-  BootNotificationResponse,
-  ErrorResponse,
-  HeartbeatResponse,
-  MeterValuesResponse,
+  type BootNotificationResponse,
+  type ErrorResponse,
+  type HeartbeatResponse,
+  type MeterValuesResponse,
   RegistrationStatus,
-  Response,
-  StatusNotificationResponse,
+  type Response,
+  type StatusNotificationResponse,
 } from '../types/ocpp/Responses';
 import {
   StopTransactionReason,
-  StopTransactionRequest,
-  StopTransactionResponse,
+  type StopTransactionRequest,
+  type StopTransactionResponse,
 } from '../types/ocpp/Transaction';
 import { WSError, WebSocketCloseEventStatusCode } from '../types/WebSocket';
 import Configuration from '../utils/Configuration';
@@ -80,6 +82,9 @@ import OCPP16IncomingRequestService from './ocpp/1.6/OCPP16IncomingRequestServic
 import OCPP16RequestService from './ocpp/1.6/OCPP16RequestService';
 import OCPP16ResponseService from './ocpp/1.6/OCPP16ResponseService';
 import { OCPP16ServiceUtils } from './ocpp/1.6/OCPP16ServiceUtils';
+import OCPP20IncomingRequestService from './ocpp/2.0/OCPP20IncomingRequestService';
+import OCPP20RequestService from './ocpp/2.0/OCPP20RequestService';
+import OCPP20ResponseService from './ocpp/2.0/OCPP20ResponseService';
 import type OCPPIncomingRequestService from './ocpp/OCPPIncomingRequestService';
 import type OCPPRequestService from './ocpp/OCPPRequestService';
 import SharedLRUCache from './SharedLRUCache';
@@ -89,6 +94,7 @@ export default class ChargingStation {
   public readonly templateFile: string;
   public stationInfo!: ChargingStationInfo;
   public started: boolean;
+  public starting: boolean;
   public authorizedTagsCache: AuthorizedTagsCache;
   public automaticTransactionGenerator!: AutomaticTransactionGenerator;
   public ocppConfiguration!: ChargingStationOcppConfiguration;
@@ -101,7 +107,6 @@ export default class ChargingStation {
   public bootNotificationRequest!: BootNotificationRequest;
   public bootNotificationResponse!: BootNotificationResponse | null;
   public powerDivider!: number;
-  private starting: boolean;
   private stopping: boolean;
   private configurationFile!: string;
   private configurationFileHash!: string;
@@ -606,6 +611,12 @@ export default class ChargingStation {
     options.handshakeTimeout = options?.handshakeTimeout ?? this.getConnectionTimeout() * 1000;
     params.closeOpened = params?.closeOpened ?? false;
     params.terminateOpened = params?.terminateOpened ?? false;
+    if (this.started === false && this.starting === false) {
+      logger.warn(
+        `${this.logPrefix()} Cannot open OCPP connection to URL ${this.wsConnectionUrl.toString()} on stopped charging station`
+      );
+      return;
+    }
     if (
       !Utils.isNullOrUndefined(this.stationInfo.supervisionUser) &&
       !Utils.isNullOrUndefined(this.stationInfo.supervisionPassword)
@@ -618,13 +629,16 @@ export default class ChargingStation {
     if (params?.terminateOpened) {
       this.terminateWSConnection();
     }
+    const ocppVersion = this.getOcppVersion();
     let protocol: string;
-    switch (this.getOcppVersion()) {
+    switch (ocppVersion) {
       case OCPPVersion.VERSION_16:
-        protocol = 'ocpp' + OCPPVersion.VERSION_16;
+      case OCPPVersion.VERSION_20:
+      case OCPPVersion.VERSION_201:
+        protocol = 'ocpp' + ocppVersion;
         break;
       default:
-        this.handleUnsupportedVersion(this.getOcppVersion());
+        this.handleUnsupportedVersion(ocppVersion);
         break;
     }
 
@@ -788,7 +802,7 @@ export default class ChargingStation {
   private getStationInfoFromTemplate(): ChargingStationInfo {
     const stationTemplate: ChargingStationTemplate = this.getTemplateFromFile();
     if (Utils.isNullOrUndefined(stationTemplate)) {
-      const errorMsg = 'Failed to read charging station template file';
+      const errorMsg = `Failed to read charging station template file ${this.templateFile}`;
       logger.error(`${this.logPrefix()} ${errorMsg}`);
       throw new BaseError(errorMsg);
     }
@@ -810,6 +824,21 @@ export default class ChargingStation {
       'supervisionUrl',
       'supervisionUrls'
     );
+    const firmwareVersionRegExp = stationTemplate.firmwareVersionPattern
+      ? new RegExp(stationTemplate.firmwareVersionPattern)
+      : Constants.SEMVER_REGEXP;
+    if (
+      stationTemplate.firmwareVersion &&
+      firmwareVersionRegExp.test(stationTemplate.firmwareVersion) === false
+    ) {
+      logger.warn(
+        `${this.logPrefix()} Firmware version '${
+          stationTemplate.firmwareVersion
+        }' in template file ${
+          this.templateFile
+        } does not match regular expression '${firmwareVersionRegExp.toString()}'`
+      );
+    }
     const stationInfo: ChargingStationInfo =
       ChargingStationUtils.stationTemplateToStationInfo(stationTemplate);
     stationInfo.hashId = ChargingStationUtils.getHashId(this.index, stationTemplate);
@@ -951,6 +980,14 @@ export default class ChargingStation {
           OCPP16ResponseService.getInstance<OCPP16ResponseService>()
         );
         break;
+      case OCPPVersion.VERSION_20:
+      case OCPPVersion.VERSION_201:
+        this.ocppIncomingRequestService =
+          OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>();
+        this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
+          OCPP20ResponseService.getInstance<OCPP20ResponseService>()
+        );
+        break;
       default:
         this.handleUnsupportedVersion(this.getOcppVersion());
         break;
@@ -1379,7 +1416,7 @@ export default class ChargingStation {
     }
   }
 
-  private async onClose(code: number, reason: string): Promise<void> {
+  private async onClose(code: number, reason: Buffer): Promise<void> {
     switch (code) {
       // Normal close
       case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
@@ -1387,7 +1424,7 @@ export default class ChargingStation {
         logger.info(
           `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
             code
-          )}' and reason '${reason}'`
+          )}' and reason '${reason.toString()}'`
         );
         this.autoReconnectRetryCount = 0;
         break;
@@ -1396,7 +1433,7 @@ export default class ChargingStation {
         logger.error(
           `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
             code
-          )}' and reason '${reason}'`
+          )}' and reason '${reason.toString()}'`
         );
         this.started === true && (await this.reconnect());
         break;
@@ -1404,7 +1441,7 @@ export default class ChargingStation {
     parentPort.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
   }
 
-  private async onMessage(data: Data): Promise<void> {
+  private async onMessage(data: RawData): Promise<void> {
     let messageType: number;
     let messageId: string;
     let commandName: IncomingRequestCommand;
@@ -1412,8 +1449,8 @@ export default class ChargingStation {
     let errorType: ErrorType;
     let errorMessage: string;
     let errorDetails: JsonType;
-    let responseCallback: (payload: JsonType, requestPayload: JsonType) => void;
-    let errorCallback: (error: OCPPError, requestStatistic?: boolean) => void;
+    let responseCallback: ResponseCallback;
+    let errorCallback: ErrorCallback;
     let requestCommandName: RequestCommand | IncomingRequestCommand;
     let requestPayload: JsonType;
     let cachedRequest: CachedRequest;
@@ -1882,9 +1919,7 @@ export default class ChargingStation {
   }
 
   private getConfiguredSupervisionUrl(): URL {
-    const supervisionUrls = Utils.cloneObject<string | string[]>(
-      this.stationInfo.supervisionUrls ?? Configuration.getSupervisionUrls()
-    );
+    const supervisionUrls = this.stationInfo.supervisionUrls ?? Configuration.getSupervisionUrls();
     if (!Utils.isEmptyArray(supervisionUrls)) {
       switch (Configuration.getSupervisionUrlDistribution()) {
         case SupervisionUrlDistribution.ROUND_ROBIN: