Merge branch 'master' of github.com:LucasBrazi06/ev-simulator into master-enterprise
authorJérôme Benoit <jerome.benoit@sap.com>
Mon, 1 Feb 2021 18:54:52 +0000 (19:54 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Mon, 1 Feb 2021 18:54:52 +0000 (19:54 +0100)
46 files changed:
package-lock.json
package.json
src/assets/configs-aws
src/charging-station/AutomaticTransactionGenerator.ts
src/charging-station/Bootstrap.ts [new file with mode: 0644]
src/charging-station/ChargingStation.ts
src/charging-station/StationWorker.ts
src/charging-station/ocpp/1.6/OCCP16IncomingRequestService.ts [new file with mode: 0644]
src/charging-station/ocpp/1.6/OCPP16RequestService.ts [new file with mode: 0644]
src/charging-station/ocpp/1.6/OCPP16ResponseService.ts [new file with mode: 0644]
src/charging-station/ocpp/OCPPIncomingRequestService.ts [new file with mode: 0644]
src/charging-station/ocpp/OCPPRequestService.ts [new file with mode: 0644]
src/charging-station/ocpp/OCPPResponseService.ts [new file with mode: 0644]
src/scripts/deleteChargingStations.js [moved from src/scripts/deleteChargingStations.ts with 83% similarity, mode: 0755]
src/scripts/setCSPublicFlag.js [moved from src/scripts/setCSPublicFlag.ts with 84% similarity, mode: 0755]
src/start.ts
src/types/ChargingStationTemplate.ts
src/types/Connectors.ts
src/types/Worker.ts
src/types/ocpp/1.6/ChargePointErrorCode.ts
src/types/ocpp/1.6/ChargePointStatus.ts
src/types/ocpp/1.6/ChargingProfile.ts
src/types/ocpp/1.6/Configuration.ts
src/types/ocpp/1.6/MeterValues.ts
src/types/ocpp/1.6/RequestResponses.ts
src/types/ocpp/1.6/Requests.ts
src/types/ocpp/1.6/Transaction.ts
src/types/ocpp/ChargePointErrorCode.ts [new file with mode: 0644]
src/types/ocpp/ChargePointStatus.ts [new file with mode: 0644]
src/types/ocpp/ChargingProfile.ts [new file with mode: 0644]
src/types/ocpp/Configuration.ts
src/types/ocpp/MeterValues.ts [new file with mode: 0644]
src/types/ocpp/OCPPProtocol.ts [new file with mode: 0644]
src/types/ocpp/OCPPVersion.ts [new file with mode: 0644]
src/types/ocpp/RequestResponses.ts [new file with mode: 0644]
src/types/ocpp/Requests.ts
src/types/ocpp/Transaction.ts [new file with mode: 0644]
src/utils/CircularArray.ts
src/utils/Configuration.ts
src/utils/Constants.ts
src/utils/Statistics.ts
src/worker/WorkerDynamicPool.ts
src/worker/WorkerFactory.ts
src/worker/WorkerSet.ts
src/worker/WorkerStaticPool.ts
src/worker/Wrk.ts

index ac1da04f088ca17a3d0fec6c307e1190c4c86d7e..f4a2d2dfb9fda860ec9afa822d07dde08fceae5b 100644 (file)
       }
     },
     "eslint": {
-      "version": "7.18.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.18.0.tgz",
-      "integrity": "sha512-fbgTiE8BfUJZuBeq2Yi7J3RB3WGUQ9PNuNbmgi6jt9Iv8qrkxfy19Ds3OpL1Pm7zg3BtTVhvcUZbIRQ0wmSjAQ==",
+      "version": "7.19.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.19.0.tgz",
+      "integrity": "sha512-CGlMgJY56JZ9ZSYhJuhow61lMPPjUzWmChFya71Z/jilVos7mR/jPgaEfVGgMBY5DshbKdG8Ezb8FDCHcoMEMg==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.0.0",
       }
     },
     "flatted": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.0.tgz",
-      "integrity": "sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==",
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz",
+      "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==",
       "dev": true
     },
     "fn.name": {
index 39c20193be44ac5a73221d61025589f28a6bd54a..73fe89012e2f25c9a3f381508350b331296e91f6 100644 (file)
@@ -68,7 +68,7 @@
     "@typescript-eslint/parser": "^4.14.1",
     "clinic": "^8.0.1",
     "cross-env": "^7.0.3",
-    "eslint": "^7.18.0",
+    "eslint": "^7.19.0",
     "grunt": "^1.3.0",
     "grunt-ts": "^6.0.0-beta.22",
     "mbt": "^1.1.0",
index 5c1897ea8127c73f6e063c2267214089719da194..03f3aba699def4fdfc3926674e469aef37e444d9 160000 (submodule)
@@ -1 +1 @@
-Subproject commit 5c1897ea8127c73f6e063c2267214089719da194
+Subproject commit 03f3aba699def4fdfc3926674e469aef37e444d9
index 76758c14ea1a1cd6e22fbf968105865f98688617..763533ed82e12269cb56183708bf0ae7720ab6a0 100644 (file)
@@ -1,4 +1,4 @@
-import { AuthorizationStatus, AuthorizeResponse, StartTransactionResponse, StopTransactionReason, StopTransactionResponse } from '../types/ocpp/1.6/Transaction';
+import { AuthorizationStatus, AuthorizeResponse, StartTransactionResponse, StopTransactionReason, StopTransactionResponse } from '../types/ocpp/Transaction';
 import { PerformanceObserver, performance } from 'perf_hooks';
 
 import ChargingStation from './ChargingStation';
@@ -23,62 +23,63 @@ export default class AutomaticTransactionGenerator {
     }
   }
 
-  _logPrefix(connectorId: number = null): string {
-    if (connectorId) {
-      return Utils.logPrefix(' ' + this.chargingStation.stationInfo.chargingStationId + ' ATG on connector #' + connectorId.toString() + ':');
-    }
-    return Utils.logPrefix(' ' + this.chargingStation.stationInfo.chargingStationId + ' ATG:');
-  }
-
-  start(): void {
+  public async start(): Promise<void> {
     this.timeToStop = false;
     if (this.chargingStation.stationInfo.AutomaticTransactionGenerator.stopAfterHours &&
       this.chargingStation.stationInfo.AutomaticTransactionGenerator.stopAfterHours > 0) {
-      setTimeout(() => {
-        void this.stop();
+      // eslint-disable-next-line @typescript-eslint/no-misused-promises
+      setTimeout(async (): Promise<void> => {
+        await this.stop();
       }, this.chargingStation.stationInfo.AutomaticTransactionGenerator.stopAfterHours * 3600 * 1000);
     }
     for (const connector in this.chargingStation.connectors) {
       if (Utils.convertToInt(connector) > 0) {
-        void this.startConnector(Utils.convertToInt(connector));
+        await this.startConnector(Utils.convertToInt(connector));
       }
     }
-    logger.info(this._logPrefix() + ' ATG started and will stop in ' + Utils.secondsToHHMMSS(this.chargingStation.stationInfo.AutomaticTransactionGenerator.stopAfterHours * 3600));
+    logger.info(this.logPrefix() + ' ATG started and will stop in ' + Utils.secondsToHHMMSS(this.chargingStation.stationInfo.AutomaticTransactionGenerator.stopAfterHours * 3600));
   }
 
-  async stop(reason: StopTransactionReason = StopTransactionReason.NONE): Promise<void> {
-    logger.info(this._logPrefix() + ' ATG OVER => STOPPING ALL TRANSACTIONS');
+  public async stop(reason: StopTransactionReason = StopTransactionReason.NONE): Promise<void> {
+    logger.info(this.logPrefix() + ' ATG OVER => STOPPING ALL TRANSACTIONS');
     for (const connector in this.chargingStation.connectors) {
+      const transactionId = this.chargingStation.getConnector(Utils.convertToInt(connector)).transactionId;
       if (this.chargingStation.getConnector(Utils.convertToInt(connector)).transactionStarted) {
-        logger.info(this._logPrefix(Utils.convertToInt(connector)) + ' ATG OVER. Stop transaction ' + this.chargingStation.getConnector(Utils.convertToInt(connector)).transactionId.toString());
-        await this.chargingStation.sendStopTransaction(this.chargingStation.getConnector(Utils.convertToInt(connector)).transactionId, reason);
+        logger.info(this.logPrefix(Utils.convertToInt(connector)) + ' ATG OVER. Stop transaction ' + transactionId.toString());
+        await this.chargingStation.ocppRequestService.sendStopTransaction(transactionId, this.chargingStation.getTransactionMeterStop(transactionId), this.chargingStation.getTransactionIdTag(transactionId), reason);
       }
     }
     this.timeToStop = true;
   }
 
-  async startConnector(connectorId: number): Promise<void> {
+  public async startConnector(connectorId: number): Promise<void> {
     do {
       if (this.timeToStop) {
-        logger.error(this._logPrefix(connectorId) + ' Entered in transaction loop while a request to stop it was made');
+        logger.error(this.logPrefix(connectorId) + ' Entered in transaction loop while a request to stop it was made');
         break;
       }
-      if (!this.chargingStation._isRegistered()) {
-        logger.error(this._logPrefix(connectorId) + ' Entered in transaction loop while the charging station is not registered');
+      if (!this.chargingStation.isRegistered()) {
+        logger.error(this.logPrefix(connectorId) + ' Entered in transaction loop while the charging station is not registered');
         break;
       }
-      if (!this.chargingStation._isChargingStationAvailable()) {
-        logger.info(this._logPrefix(connectorId) + ' Entered in transaction loop while the charging station is unavailable');
+      if (!this.chargingStation.isChargingStationAvailable()) {
+        logger.info(this.logPrefix(connectorId) + ' Entered in transaction loop while the charging station is unavailable');
         await this.stop();
         break;
       }
-      if (!this.chargingStation._isConnectorAvailable(connectorId)) {
-        logger.info(`${this._logPrefix(connectorId)} Entered in transaction loop while the connector ${connectorId} is unavailable, stop it`);
+      if (!this.chargingStation.isConnectorAvailable(connectorId)) {
+        logger.info(`${this.logPrefix(connectorId)} Entered in transaction loop while the connector ${connectorId} is unavailable, stop it`);
         break;
       }
+      if (!this.chargingStation?.ocppRequestService) {
+        logger.info(`${this.logPrefix(connectorId)} Transaction loop waiting for charging station service to be initialized`);
+        do {
+          await Utils.sleep(Constants.CHARGING_STATION_ATG_INITIALIZATION_TIME);
+        } while (!this.chargingStation?.ocppRequestService);
+      }
       const wait = Utils.getRandomInt(this.chargingStation.stationInfo.AutomaticTransactionGenerator.maxDelayBetweenTwoTransactions,
         this.chargingStation.stationInfo.AutomaticTransactionGenerator.minDelayBetweenTwoTransactions) * 1000;
-      logger.info(this._logPrefix(connectorId) + ' wait for ' + Utils.milliSecondsToHHMMSS(wait));
+      logger.info(this.logPrefix(connectorId) + ' wait for ' + Utils.milliSecondsToHHMMSS(wait));
       await Utils.sleep(wait);
       const start = Math.random();
       let skip = 0;
@@ -94,17 +95,17 @@ export default class AutomaticTransactionGenerator {
           startResponse = await this.startTransaction(connectorId, this);
         }
         if (startResponse?.idTagInfo?.status !== AuthorizationStatus.ACCEPTED) {
-          logger.info(this._logPrefix(connectorId) + ' transaction rejected');
+          logger.info(this.logPrefix(connectorId) + ' transaction rejected');
           await Utils.sleep(Constants.CHARGING_STATION_ATG_WAIT_TIME);
         } else {
           // Wait until end of transaction
           const waitTrxEnd = Utils.getRandomInt(this.chargingStation.stationInfo.AutomaticTransactionGenerator.maxDuration,
             this.chargingStation.stationInfo.AutomaticTransactionGenerator.minDuration) * 1000;
-          logger.info(this._logPrefix(connectorId) + ' transaction ' + this.chargingStation.getConnector(connectorId).transactionId.toString() + ' will stop in ' + Utils.milliSecondsToHHMMSS(waitTrxEnd));
+          logger.info(this.logPrefix(connectorId) + ' transaction ' + this.chargingStation.getConnector(connectorId).transactionId.toString() + ' will stop in ' + Utils.milliSecondsToHHMMSS(waitTrxEnd));
           await Utils.sleep(waitTrxEnd);
           // Stop transaction
           if (this.chargingStation.getConnector(connectorId)?.transactionStarted) {
-            logger.info(this._logPrefix(connectorId) + ' stop transaction ' + this.chargingStation.getConnector(connectorId).transactionId.toString());
+            logger.info(this.logPrefix(connectorId) + ' stop transaction ' + this.chargingStation.getConnector(connectorId).transactionId.toString());
             if (this.chargingStation.getEnableStatistics()) {
               const stopTransaction = performance.timerify(this.stopTransaction);
               this.performanceObserver.observe({ entryTypes: ['function'] });
@@ -116,10 +117,10 @@ export default class AutomaticTransactionGenerator {
         }
       } else {
         skip++;
-        logger.info(this._logPrefix(connectorId) + ' transaction skipped ' + skip.toString());
+        logger.info(this.logPrefix(connectorId) + ' transaction skipped ' + skip.toString());
       }
     } while (!this.timeToStop);
-    logger.info(this._logPrefix(connectorId) + ' ATG STOPPED on the connector');
+    logger.info(this.logPrefix(connectorId) + ' ATG STOPPED on the connector');
   }
 
   // eslint-disable-next-line consistent-this
@@ -128,24 +129,32 @@ export default class AutomaticTransactionGenerator {
       const tagId = self.chargingStation.getRandomTagId();
       if (self.chargingStation.stationInfo.AutomaticTransactionGenerator.requireAuthorize) {
         // Authorize tagId
-        const authorizeResponse = await self.chargingStation.sendAuthorize(tagId);
+        const authorizeResponse = await self.chargingStation.ocppRequestService.sendAuthorize(tagId);
         if (authorizeResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
-          logger.info(self._logPrefix(connectorId) + ' start transaction for tagID ' + tagId);
+          logger.info(self.logPrefix(connectorId) + ' start transaction for tagID ' + tagId);
           // Start transaction
-          return await self.chargingStation.sendStartTransaction(connectorId, tagId);
+          return await self.chargingStation.ocppRequestService.sendStartTransaction(connectorId, tagId);
         }
         return authorizeResponse;
       }
-      logger.info(self._logPrefix(connectorId) + ' start transaction for tagID ' + tagId);
+      logger.info(self.logPrefix(connectorId) + ' start transaction for tagID ' + tagId);
       // Start transaction
-      return await self.chargingStation.sendStartTransaction(connectorId, tagId);
+      return await self.chargingStation.ocppRequestService.sendStartTransaction(connectorId, tagId);
     }
-    logger.info(self._logPrefix(connectorId) + ' start transaction without a tagID');
-    return await self.chargingStation.sendStartTransaction(connectorId);
+    logger.info(self.logPrefix(connectorId) + ' start transaction without a tagID');
+    return await self.chargingStation.ocppRequestService.sendStartTransaction(connectorId);
   }
 
   // eslint-disable-next-line consistent-this
   private async stopTransaction(connectorId: number, self: AutomaticTransactionGenerator): Promise<StopTransactionResponse> {
-    return await self.chargingStation.sendStopTransaction(self.chargingStation.getConnector(connectorId).transactionId);
+    const transactionId = self.chargingStation.getConnector(connectorId).transactionId;
+    return await self.chargingStation.ocppRequestService.sendStopTransaction(transactionId, self.chargingStation.getTransactionMeterStop(transactionId), self.chargingStation.getTransactionIdTag(transactionId));
+  }
+
+  private logPrefix(connectorId: number = null): string {
+    if (connectorId) {
+      return Utils.logPrefix(' ' + this.chargingStation.stationInfo.chargingStationId + ' ATG on connector #' + connectorId.toString() + ':');
+    }
+    return Utils.logPrefix(' ' + this.chargingStation.stationInfo.chargingStationId + ' ATG:');
   }
 }
diff --git a/src/charging-station/Bootstrap.ts b/src/charging-station/Bootstrap.ts
new file mode 100644 (file)
index 0000000..13fb957
--- /dev/null
@@ -0,0 +1,91 @@
+import Configuration from '../utils/Configuration';
+import { StationWorkerData } from '../types/Worker';
+import Utils from '../utils/Utils';
+import WorkerFactory from '../worker/WorkerFactory';
+import Wrk from '../worker/Wrk';
+import { isMainThread } from 'worker_threads';
+
+export default class Bootstrap {
+  private static instance: Bootstrap;
+  private isStarted: boolean;
+  private workerScript: string;
+  private workerImplementationInstance: Wrk;
+
+  private constructor() {
+    this.isStarted = false;
+    this.workerScript = './dist/charging-station/StationWorker.js';
+  }
+
+  public static getInstance(): Bootstrap {
+    if (!Bootstrap.instance) {
+      Bootstrap.instance = new Bootstrap();
+    }
+    return Bootstrap.instance;
+  }
+
+  public async start(): Promise<void> {
+    if (isMainThread && !this.isStarted) {
+      try {
+        let numStationsTotal = 0;
+        await this.getWorkerImplementationInstance().start();
+        // Start ChargingStation object in worker thread
+        if (Configuration.getStationTemplateURLs()) {
+          for (const stationURL of Configuration.getStationTemplateURLs()) {
+            try {
+              const nbStations = stationURL.numberOfStations ? stationURL.numberOfStations : 0;
+              for (let index = 1; index <= nbStations; index++) {
+                const workerData: StationWorkerData = {
+                  index,
+                  templateFile: stationURL.file
+                };
+                await this.getWorkerImplementationInstance().addElement(workerData);
+                numStationsTotal++;
+              }
+            } catch (error) {
+            // eslint-disable-next-line no-console
+              console.error('Charging station start with template file ' + stationURL.file + ' error ', error);
+            }
+          }
+        } else {
+          console.log('No stationTemplateURLs defined in configuration, exiting');
+        }
+        if (numStationsTotal === 0) {
+          console.log('No charging station template enabled in configuration, exiting');
+        } else {
+          console.log(`Charging station simulator started with ${numStationsTotal.toString()} charging station(s) and ${Utils.workerDynamicPoolInUse() ? `${Configuration.getWorkerPoolMinSize().toString()}/` : ''}${this.getWorkerImplementationInstance().size}${Utils.workerPoolInUse() ? `/${Configuration.getWorkerPoolMaxSize().toString()}` : ''} worker(s) concurrently running in '${Configuration.getWorkerProcess()}' mode (${this.getWorkerImplementationInstance().maxElementsPerWorker} charging station(s) per worker)`);
+        }
+        this.isStarted = true;
+      } catch (error) {
+      // eslint-disable-next-line no-console
+        console.error('Bootstrap start error ', error);
+      }
+    }
+  }
+
+  public async stop(): Promise<void> {
+    if (isMainThread && this.isStarted) {
+      await this.getWorkerImplementationInstance().stop();
+      if (this.getWorkerImplementationInstance()) {
+        // Nullify to force worker implementation instance creation
+        this.workerImplementationInstance = null;
+      }
+    }
+    this.isStarted = false;
+  }
+
+  public async restart(): Promise<void> {
+    await this.stop();
+    await this.start();
+  }
+
+  private getWorkerImplementationInstance(): Wrk {
+    if (!this.workerImplementationInstance) {
+      this.workerImplementationInstance = WorkerFactory.getWorkerImplementation<StationWorkerData>(this.workerScript, Configuration.getWorkerProcess(), {
+        poolMaxSize: Configuration.getWorkerPoolMaxSize(),
+        poolMinSize: Configuration.getWorkerPoolMinSize(),
+        elementsPerWorker: Configuration.getChargingStationsPerWorker()
+      });
+    }
+    return this.workerImplementationInstance;
+  }
+}
index e419d81eb38729ccf5585fc01db3238780655aa5..a7081aa97eb68c74bc5522a557276972062fdd15 100644 (file)
@@ -1,29 +1,29 @@
-import { AuthorizationStatus, AuthorizeRequest, AuthorizeResponse, StartTransactionRequest, StartTransactionResponse, StopTransactionReason, StopTransactionRequest, StopTransactionResponse } from '../types/ocpp/1.6/Transaction';
-import { AvailabilityType, BootNotificationRequest, ChangeAvailabilityRequest, ChangeConfigurationRequest, ClearChargingProfileRequest, GetConfigurationRequest, HeartbeatRequest, IncomingRequestCommand, RemoteStartTransactionRequest, RemoteStopTransactionRequest, RequestCommand, ResetRequest, SetChargingProfileRequest, StatusNotificationRequest, UnlockConnectorRequest } from '../types/ocpp/1.6/Requests';
-import { BootNotificationResponse, ChangeAvailabilityResponse, ChangeConfigurationResponse, ClearChargingProfileResponse, DefaultResponse, GetConfigurationResponse, HeartbeatResponse, RegistrationStatus, SetChargingProfileResponse, StatusNotificationResponse, UnlockConnectorResponse } from '../types/ocpp/1.6/RequestResponses';
-import { ChargingProfile, ChargingProfilePurposeType } from '../types/ocpp/1.6/ChargingProfile';
+import { BootNotificationResponse, RegistrationStatus } from '../types/ocpp/RequestResponses';
 import ChargingStationConfiguration, { ConfigurationKey } from '../types/ChargingStationConfiguration';
 import ChargingStationTemplate, { PowerOutType, VoltageOut } from '../types/ChargingStationTemplate';
 import Connectors, { Connector } from '../types/Connectors';
-import { MeterValue, MeterValueLocation, MeterValueMeasurand, MeterValuePhase, MeterValueUnit, MeterValuesRequest, MeterValuesResponse, SampledValue } from '../types/ocpp/1.6/MeterValues';
 import { PerformanceObserver, performance } from 'perf_hooks';
-import Requests, { IncomingRequest, Request } from '../types/ocpp/Requests';
+import Requests, { AvailabilityType, BootNotificationRequest, IncomingRequest, IncomingRequestCommand } from '../types/ocpp/Requests';
 import WebSocket, { MessageEvent } from 'ws';
 
 import AutomaticTransactionGenerator from './AutomaticTransactionGenerator';
-import { ChargePointErrorCode } from '../types/ocpp/1.6/ChargePointErrorCode';
-import { ChargePointStatus } from '../types/ocpp/1.6/ChargePointStatus';
+import { ChargePointStatus } from '../types/ocpp/ChargePointStatus';
+import { ChargingProfile } from '../types/ocpp/ChargingProfile';
 import ChargingStationInfo from '../types/ChargingStationInfo';
 import Configuration from '../utils/Configuration';
 import Constants from '../utils/Constants';
-import ElectricUtils from '../utils/ElectricUtils';
-import { ErrorType } from '../types/ocpp/ErrorType';
-import MeasurandValues from '../types/MeasurandValues';
 import { MessageType } from '../types/ocpp/MessageType';
-import { OCPPConfigurationKey } from '../types/ocpp/Configuration';
+import { MeterValueMeasurand } from '../types/ocpp/MeterValues';
+import OCPP16IncomingRequestService from './ocpp/1.6/OCCP16IncomingRequestService';
+import OCPP16RequestService from './ocpp/1.6/OCPP16RequestService';
+import OCPP16ResponseService from './ocpp/1.6/OCPP16ResponseService';
 import OCPPError from './OcppError';
-import { StandardParametersKey } from '../types/ocpp/1.6/Configuration';
+import OCPPIncomingRequestService from './ocpp/OCPPIncomingRequestService';
+import OCPPRequestService from './ocpp/OCPPRequestService';
+import { OCPPVersion } from '../types/ocpp/OCPPVersion';
+import { StandardParametersKey } from '../types/ocpp/Configuration';
 import Statistics from '../utils/Statistics';
+import { StopTransactionReason } from '../types/ocpp/Transaction';
 import Utils from '../utils/Utils';
 import { WebSocketCloseEventStatusCode } from '../types/WebSocket';
 import crypto from 'crypto';
@@ -31,34 +31,36 @@ import fs from 'fs';
 import logger from '../utils/Logger';
 
 export default class ChargingStation {
+  public stationTemplateFile: string;
+  public authorizedTags: string[];
   public stationInfo: ChargingStationInfo;
   public connectors: Connectors;
+  public configuration: ChargingStationConfiguration;
+  public hasStopped: boolean;
+  public wsConnection: WebSocket;
+  public requests: Requests;
+  public messageQueue: string[];
   public statistics: Statistics;
+  public heartbeatSetInterval: NodeJS.Timeout;
+  public ocppIncomingRequestService: OCPPIncomingRequestService;
+  public ocppRequestService: OCPPRequestService;
   private index: number;
-  private stationTemplateFile: string;
   private bootNotificationRequest: BootNotificationRequest;
   private bootNotificationResponse: BootNotificationResponse;
-  private configuration: ChargingStationConfiguration;
   private connectorsConfigurationHash: string;
   private supervisionUrl: string;
   private wsConnectionUrl: string;
-  private wsConnection: WebSocket;
-  private hasStopped: boolean;
   private hasSocketRestarted: boolean;
   private autoReconnectRetryCount: number;
-  private requests: Requests;
-  private messageQueue: string[];
   private automaticTransactionGeneration: AutomaticTransactionGenerator;
-  private authorizedTags: string[];
-  private heartbeatSetInterval: NodeJS.Timeout;
-  private webSocketPingSetInterval: NodeJS.Timeout;
   private performanceObserver: PerformanceObserver;
+  private webSocketPingSetInterval: NodeJS.Timeout;
 
   constructor(index: number, stationTemplateFile: string) {
     this.index = index;
     this.stationTemplateFile = stationTemplateFile;
     this.connectors = {} as Connectors;
-    this._initialize();
+    this.initialize();
 
     this.hasStopped = false;
     this.hasSocketRestarted = false;
@@ -67,10 +69,257 @@ export default class ChargingStation {
     this.requests = {} as Requests;
     this.messageQueue = [] as string[];
 
-    this.authorizedTags = this._loadAndGetAuthorizedTags();
+    this.authorizedTags = this.getAuthorizedTags();
+  }
+
+  public logPrefix(): string {
+    return Utils.logPrefix(` ${this.stationInfo.chargingStationId}:`);
+  }
+
+  public getRandomTagId(): string {
+    const index = Math.floor(Math.random() * this.authorizedTags.length);
+    return this.authorizedTags[index];
+  }
+
+  public hasAuthorizedTags(): boolean {
+    return !Utils.isEmptyArray(this.authorizedTags);
+  }
+
+  public getEnableStatistics(): boolean {
+    return !Utils.isUndefined(this.stationInfo.enableStatistics) ? this.stationInfo.enableStatistics : true;
+  }
+
+  public getNumberOfPhases(): number {
+    switch (this.getPowerOutType()) {
+      case PowerOutType.AC:
+        return !Utils.isUndefined(this.stationInfo.numberOfPhases) ? this.stationInfo.numberOfPhases : 3;
+      case PowerOutType.DC:
+        return 0;
+    }
+  }
+
+  public isWebSocketOpen(): boolean {
+    return this.wsConnection?.readyState === WebSocket.OPEN;
+  }
+
+  public isRegistered(): boolean {
+    return this.bootNotificationResponse?.status === RegistrationStatus.ACCEPTED;
+  }
+
+  public isChargingStationAvailable(): boolean {
+    return this.getConnector(0).availability === AvailabilityType.OPERATIVE;
+  }
+
+  public isConnectorAvailable(id: number): boolean {
+    return this.getConnector(id).availability === AvailabilityType.OPERATIVE;
+  }
+
+  public getConnector(id: number): Connector {
+    return this.connectors[id];
+  }
+
+  public getPowerOutType(): PowerOutType {
+    return !Utils.isUndefined(this.stationInfo.powerOutType) ? this.stationInfo.powerOutType : PowerOutType.AC;
+  }
+
+  public getVoltageOut(): number {
+    const errMsg = `${this.logPrefix()} Unknown ${this.getPowerOutType()} powerOutType in template file ${this.stationTemplateFile}, cannot define default voltage out`;
+    let defaultVoltageOut: number;
+    switch (this.getPowerOutType()) {
+      case PowerOutType.AC:
+        defaultVoltageOut = VoltageOut.VOLTAGE_230;
+        break;
+      case PowerOutType.DC:
+        defaultVoltageOut = VoltageOut.VOLTAGE_400;
+        break;
+      default:
+        logger.error(errMsg);
+        throw Error(errMsg);
+    }
+    return !Utils.isUndefined(this.stationInfo.voltageOut) ? this.stationInfo.voltageOut : defaultVoltageOut;
+  }
+
+  public getTransactionIdTag(transactionId: number): string {
+    for (const connector in this.connectors) {
+      if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) {
+        return this.getConnector(Utils.convertToInt(connector)).idTag;
+      }
+    }
+  }
+
+  public getTransactionMeterStop(transactionId: number): number {
+    for (const connector in this.connectors) {
+      if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) {
+        return this.getConnector(Utils.convertToInt(connector)).lastEnergyActiveImportRegisterValue;
+      }
+    }
+  }
+
+  public getAuthorizeRemoteTxRequests(): boolean {
+    const authorizeRemoteTxRequests = this.getConfigurationKey(StandardParametersKey.AuthorizeRemoteTxRequests);
+    return authorizeRemoteTxRequests ? Utils.convertToBoolean(authorizeRemoteTxRequests.value) : false;
+  }
+
+  public getLocalAuthListEnabled(): boolean {
+    const localAuthListEnabled = this.getConfigurationKey(StandardParametersKey.LocalAuthListEnabled);
+    return localAuthListEnabled ? Utils.convertToBoolean(localAuthListEnabled.value) : false;
+  }
+
+  public restartWebSocketPing(): void {
+    // Stop WebSocket ping
+    this.stopWebSocketPing();
+    // Start WebSocket ping
+    this.startWebSocketPing();
+  }
+
+  public startHeartbeat(): void {
+    if (this.getHeartbeatInterval() && this.getHeartbeatInterval() > 0 && !this.heartbeatSetInterval) {
+      // eslint-disable-next-line @typescript-eslint/no-misused-promises
+      this.heartbeatSetInterval = setInterval(async (): Promise<void> => {
+        await this.ocppRequestService.sendHeartbeat();
+      }, this.getHeartbeatInterval());
+      logger.info(this.logPrefix() + ' Heartbeat started every ' + Utils.milliSecondsToHHMMSS(this.getHeartbeatInterval()));
+    } else if (this.heartbeatSetInterval) {
+      logger.info(this.logPrefix() + ' Heartbeat every ' + Utils.milliSecondsToHHMMSS(this.getHeartbeatInterval()) + ' already started');
+    } else {
+      logger.error(`${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval() ? Utils.milliSecondsToHHMMSS(this.getHeartbeatInterval()) : this.getHeartbeatInterval()}, not starting the heartbeat`);
+    }
+  }
+
+  public restartHeartbeat(): void {
+    // Stop heartbeat
+    this.stopHeartbeat();
+    // Start heartbeat
+    this.startHeartbeat();
+  }
+
+  public startMeterValues(connectorId: number, interval: number): void {
+    if (connectorId === 0) {
+      logger.error(`${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId.toString()}`);
+      return;
+    }
+    if (!this.getConnector(connectorId)) {
+      logger.error(`${this.logPrefix()} Trying to start MeterValues on non existing connector Id ${connectorId.toString()}`);
+      return;
+    }
+    if (!this.getConnector(connectorId)?.transactionStarted) {
+      logger.error(`${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction started`);
+      return;
+    } else if (this.getConnector(connectorId)?.transactionStarted && !this.getConnector(connectorId)?.transactionId) {
+      logger.error(`${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction id`);
+      return;
+    }
+    if (interval > 0) {
+      // eslint-disable-next-line @typescript-eslint/no-misused-promises
+      this.getConnector(connectorId).transactionSetInterval = setInterval(async (): Promise<void> => {
+        if (this.getEnableStatistics()) {
+          const sendMeterValues = performance.timerify(this.ocppRequestService.sendMeterValues);
+          this.performanceObserver.observe({
+            entryTypes: ['function'],
+          });
+          await sendMeterValues(connectorId, this.getConnector(connectorId).transactionId, interval, this.ocppRequestService);
+        } else {
+          await this.ocppRequestService.sendMeterValues(connectorId, this.getConnector(connectorId).transactionId, interval, this.ocppRequestService);
+        }
+      }, interval);
+    } else {
+      logger.error(`${this.logPrefix()} Charging station ${StandardParametersKey.MeterValueSampleInterval} configuration set to ${Utils.milliSecondsToHHMMSS(interval)}, not sending MeterValues`);
+    }
+  }
+
+  public start(): void {
+    this.openWSConnection();
+    // Monitor authorization file
+    this.startAuthorizationFileMonitoring();
+    // Monitor station template file
+    this.startStationTemplateFileMonitoring();
+    // Handle Socket incoming messages
+    this.wsConnection.on('message', this.onMessage.bind(this));
+    // Handle Socket error
+    this.wsConnection.on('error', this.onError.bind(this));
+    // Handle Socket close
+    this.wsConnection.on('close', this.onClose.bind(this));
+    // Handle Socket opening connection
+    this.wsConnection.on('open', this.onOpen.bind(this));
+    // Handle Socket ping
+    this.wsConnection.on('ping', this.onPing.bind(this));
+    // Handle Socket pong
+    this.wsConnection.on('pong', this.onPong.bind(this));
+  }
+
+  public async stop(reason: StopTransactionReason = StopTransactionReason.NONE): Promise<void> {
+    // Stop message sequence
+    await this.stopMessageSequence(reason);
+    for (const connector in this.connectors) {
+      if (Utils.convertToInt(connector) > 0) {
+        await this.ocppRequestService.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.UNAVAILABLE);
+        this.getConnector(Utils.convertToInt(connector)).status = ChargePointStatus.UNAVAILABLE;
+      }
+    }
+    if (this.isWebSocketOpen()) {
+      this.wsConnection.close();
+    }
+    this.bootNotificationResponse = null;
+    this.hasStopped = true;
+  }
+
+  public getConfigurationKey(key: string | StandardParametersKey, caseInsensitive = false): ConfigurationKey {
+    const configurationKey: ConfigurationKey = this.configuration.configurationKey.find((configElement) => {
+      if (caseInsensitive) {
+        return configElement.key.toLowerCase() === key.toLowerCase();
+      }
+      return configElement.key === key;
+    });
+    return configurationKey;
+  }
+
+  public addConfigurationKey(key: string | StandardParametersKey, value: string, readonly = false, visible = true, reboot = false): void {
+    const keyFound = this.getConfigurationKey(key);
+    if (!keyFound) {
+      this.configuration.configurationKey.push({
+        key,
+        readonly,
+        value,
+        visible,
+        reboot,
+      });
+    } else {
+      logger.error(`${this.logPrefix()} Trying to add an already existing configuration key: %j`, keyFound);
+    }
+  }
+
+  public setConfigurationKeyValue(key: string | StandardParametersKey, value: string): void {
+    const keyFound = this.getConfigurationKey(key);
+    if (keyFound) {
+      const keyIndex = this.configuration.configurationKey.indexOf(keyFound);
+      this.configuration.configurationKey[keyIndex].value = value;
+    } else {
+      logger.error(`${this.logPrefix()} Trying to set a value on a non existing configuration key: %j`, { key, value });
+    }
+  }
+
+  public setChargingProfile(connectorId: number, cp: ChargingProfile): boolean {
+    if (!Utils.isEmptyArray(this.getConnector(connectorId).chargingProfiles)) {
+      this.getConnector(connectorId).chargingProfiles.forEach((chargingProfile: ChargingProfile, index: number) => {
+        if (chargingProfile.chargingProfileId === cp.chargingProfileId
+          || (chargingProfile.stackLevel === cp.stackLevel && chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)) {
+          this.getConnector(connectorId).chargingProfiles[index] = cp;
+          return true;
+        }
+      });
+    }
+    this.getConnector(connectorId).chargingProfiles.push(cp);
+    return true;
+  }
+
+  public resetTransactionOnConnector(connectorId: number): void {
+    this.initTransactionOnConnector(connectorId);
+    if (this.getConnector(connectorId)?.transactionSetInterval) {
+      clearInterval(this.getConnector(connectorId).transactionSetInterval);
+    }
   }
 
-  _getChargingStationId(stationTemplate: ChargingStationTemplate): string {
+  private getChargingStationId(stationTemplate: ChargingStationTemplate): string {
     // In case of multiple instances: add instance index to charging station id
     let instanceIndex = process.env.CF_INSTANCE_INDEX ? process.env.CF_INSTANCE_INDEX : 0;
     instanceIndex = instanceIndex > 0 ? instanceIndex : '';
@@ -78,7 +327,7 @@ export default class ChargingStation {
     return stationTemplate.fixedName ? stationTemplate.baseName : stationTemplate.baseName + '-' + instanceIndex.toString() + ('000000000' + this.index.toString()).substr(('000000000' + this.index.toString()).length - 4) + idSuffix;
   }
 
-  _buildStationInfo(): ChargingStationInfo {
+  private buildStationInfo(): ChargingStationInfo {
     let stationTemplateFromFile: ChargingStationTemplate;
     try {
       // Load template file
@@ -96,37 +345,47 @@ export default class ChargingStation {
     } else {
       stationInfo.maxPower = stationTemplateFromFile.power as number;
     }
-    stationInfo.chargingStationId = this._getChargingStationId(stationTemplateFromFile);
+    stationInfo.chargingStationId = this.getChargingStationId(stationTemplateFromFile);
     stationInfo.resetTime = stationTemplateFromFile.resetTime ? stationTemplateFromFile.resetTime * 1000 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
     return stationInfo;
   }
 
-  _initialize(): void {
-    this.stationInfo = this._buildStationInfo();
+  private getOCPPVersion(): OCPPVersion {
+    return this.stationInfo.ocppVersion ? this.stationInfo.ocppVersion : OCPPVersion.VERSION_16;
+  }
+
+  private handleUnsupportedVersion(version: OCPPVersion) {
+    const errMsg = `${this.logPrefix()} Unsupported protocol version '${version}' configured in template file ${this.stationTemplateFile}`;
+    logger.error(errMsg);
+    throw new Error(errMsg);
+  }
+
+  private initialize(): void {
+    this.stationInfo = this.buildStationInfo();
     this.bootNotificationRequest = {
       chargePointModel: this.stationInfo.chargePointModel,
       chargePointVendor: this.stationInfo.chargePointVendor,
       ...!Utils.isUndefined(this.stationInfo.chargeBoxSerialNumberPrefix) && { chargeBoxSerialNumber: this.stationInfo.chargeBoxSerialNumberPrefix },
       ...!Utils.isUndefined(this.stationInfo.firmwareVersion) && { firmwareVersion: this.stationInfo.firmwareVersion },
     };
-    this.configuration = this._getTemplateChargingStationConfiguration();
-    this.supervisionUrl = this._getSupervisionURL();
+    this.configuration = this.getTemplateChargingStationConfiguration();
+    this.supervisionUrl = this.getSupervisionURL();
     this.wsConnectionUrl = this.supervisionUrl + '/' + this.stationInfo.chargingStationId;
     // Build connectors if needed
-    const maxConnectors = this._getMaxNumberOfConnectors();
+    const maxConnectors = this.getMaxNumberOfConnectors();
     if (maxConnectors <= 0) {
-      logger.warn(`${this._logPrefix()} Charging station template ${this.stationTemplateFile} with ${maxConnectors} connectors`);
+      logger.warn(`${this.logPrefix()} Charging station template ${this.stationTemplateFile} with ${maxConnectors} connectors`);
     }
-    const templateMaxConnectors = this._getTemplateMaxNumberOfConnectors();
+    const templateMaxConnectors = this.getTemplateMaxNumberOfConnectors();
     if (templateMaxConnectors <= 0) {
-      logger.warn(`${this._logPrefix()} Charging station template ${this.stationTemplateFile} with no connector configuration`);
+      logger.warn(`${this.logPrefix()} Charging station template ${this.stationTemplateFile} with no connector configuration`);
     }
     if (!this.stationInfo.Connectors[0]) {
-      logger.warn(`${this._logPrefix()} Charging station template ${this.stationTemplateFile} with no connector Id 0 configuration`);
+      logger.warn(`${this.logPrefix()} Charging station template ${this.stationTemplateFile} with no connector Id 0 configuration`);
     }
     // Sanity check
     if (maxConnectors > (this.stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) && !this.stationInfo.randomConnectors) {
-      logger.warn(`${this._logPrefix()} Number of connectors exceeds the number of connector configurations in template ${this.stationTemplateFile}, forcing random connector configurations affectation`);
+      logger.warn(`${this.logPrefix()} Number of connectors exceeds the number of connector configurations in template ${this.stationTemplateFile}, forcing random connector configurations affectation`);
       this.stationInfo.randomConnectors = true;
     }
     const connectorsConfigHash = crypto.createHash('sha256').update(JSON.stringify(this.stationInfo.Connectors) + maxConnectors.toString()).digest('hex');
@@ -136,7 +395,7 @@ export default class ChargingStation {
       // Add connector Id 0
       let lastConnector = '0';
       for (lastConnector in this.stationInfo.Connectors) {
-        if (Utils.convertToInt(lastConnector) === 0 && this._getUseConnectorId0() && this.stationInfo.Connectors[lastConnector]) {
+        if (Utils.convertToInt(lastConnector) === 0 && this.getUseConnectorId0() && this.stationInfo.Connectors[lastConnector]) {
           this.connectors[lastConnector] = Utils.cloneObject<Connector>(this.stationInfo.Connectors[lastConnector]);
           this.connectors[lastConnector].availability = AvailabilityType.OPERATIVE;
           if (Utils.isUndefined(this.connectors[lastConnector]?.chargingProfiles)) {
@@ -161,15 +420,24 @@ export default class ChargingStation {
     // Initialize transaction attributes on connectors
     for (const connector in this.connectors) {
       if (Utils.convertToInt(connector) > 0 && !this.getConnector(Utils.convertToInt(connector)).transactionStarted) {
-        this._initTransactionOnConnector(Utils.convertToInt(connector));
+        this.initTransactionOnConnector(Utils.convertToInt(connector));
       }
     }
+    switch (this.getOCPPVersion()) {
+      case OCPPVersion.VERSION_16:
+        this.ocppIncomingRequestService = new OCPP16IncomingRequestService(this);
+        this.ocppRequestService = new OCPP16RequestService(this, new OCPP16ResponseService(this));
+        break;
+      default:
+        this.handleUnsupportedVersion(this.getOCPPVersion());
+        break;
+    }
     // OCPP parameters
-    this._addConfigurationKey(StandardParametersKey.NumberOfConnectors, this._getNumberOfConnectors().toString(), true);
-    if (!this._getConfigurationKey(StandardParametersKey.MeterValuesSampledData)) {
-      this._addConfigurationKey(StandardParametersKey.MeterValuesSampledData, MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER);
+    this.addConfigurationKey(StandardParametersKey.NumberOfConnectors, this.getNumberOfConnectors().toString(), true);
+    if (!this.getConfigurationKey(StandardParametersKey.MeterValuesSampledData)) {
+      this.addConfigurationKey(StandardParametersKey.MeterValuesSampledData, MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER);
     }
-    this.stationInfo.powerDivider = this._getPowerDivider();
+    this.stationInfo.powerDivider = this.getPowerDivider();
     if (this.getEnableStatistics()) {
       this.statistics = new Statistics(this.stationInfo.chargingStationId);
       this.performanceObserver = new PerformanceObserver((list) => {
@@ -180,73 +448,163 @@ export default class ChargingStation {
     }
   }
 
-  _logPrefix(): string {
-    return Utils.logPrefix(` ${this.stationInfo.chargingStationId}:`);
-  }
-
-  _isWebSocketOpen(): boolean {
-    return this.wsConnection?.readyState === WebSocket.OPEN;
-  }
-
-  _isRegistered(): boolean {
-    return this.bootNotificationResponse?.status === RegistrationStatus.ACCEPTED;
-  }
-
-  _getTemplateChargingStationConfiguration(): ChargingStationConfiguration {
-    return this.stationInfo.Configuration ? this.stationInfo.Configuration : {} as ChargingStationConfiguration;
-  }
-
-  _getAuthorizationFile(): string {
-    return this.stationInfo.authorizationFile && this.stationInfo.authorizationFile;
-  }
-
-  _getUseConnectorId0(): boolean {
-    return !Utils.isUndefined(this.stationInfo.useConnectorId0) ? this.stationInfo.useConnectorId0 : true;
-  }
-
-  _loadAndGetAuthorizedTags(): string[] {
-    let authorizedTags: string[] = [];
-    const authorizationFile = this._getAuthorizationFile();
-    if (authorizationFile) {
-      try {
-        // Load authorization file
-        const fileDescriptor = fs.openSync(authorizationFile, 'r');
-        authorizedTags = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8')) as string[];
-        fs.closeSync(fileDescriptor);
-      } catch (error) {
-        logger.error(this._logPrefix() + ' Authorization file ' + authorizationFile + ' loading error: %j', error);
-        throw error;
+  private async onOpen(): Promise<void> {
+    logger.info(`${this.logPrefix()} Is connected to server through ${this.wsConnectionUrl}`);
+    if (!this.isRegistered()) {
+      // Send BootNotification
+      let registrationRetryCount = 0;
+      do {
+        this.bootNotificationResponse = await this.ocppRequestService.sendBootNotification(this.bootNotificationRequest.chargePointModel, this.bootNotificationRequest.chargePointVendor, this.bootNotificationRequest.chargeBoxSerialNumber, this.bootNotificationRequest.firmwareVersion);
+        if (!this.isRegistered()) {
+          registrationRetryCount++;
+          await Utils.sleep(this.bootNotificationResponse?.interval ? this.bootNotificationResponse.interval * 1000 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL);
+        }
+      } while (!this.isRegistered() && (registrationRetryCount <= this.getRegistrationMaxRetries() || this.getRegistrationMaxRetries() === -1));
+    } else if (this.isRegistered()) {
+      await this.startMessageSequence();
+      this.hasStopped && (this.hasStopped = false);
+      if (this.hasSocketRestarted && this.isWebSocketOpen()) {
+        if (!Utils.isEmptyArray(this.messageQueue)) {
+          this.messageQueue.forEach((message, index) => {
+            this.messageQueue.splice(index, 1);
+            this.wsConnection.send(message);
+          });
+        }
       }
     } else {
-      logger.info(this._logPrefix() + ' No authorization file given in template file ' + this.stationTemplateFile);
+      logger.error(`${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`);
     }
-    return authorizedTags;
-  }
-
-  getRandomTagId(): string {
-    const index = Math.floor(Math.random() * this.authorizedTags.length);
-    return this.authorizedTags[index];
-  }
-
-  hasAuthorizedTags(): boolean {
-    return !Utils.isEmptyArray(this.authorizedTags);
-  }
-
-  getEnableStatistics(): boolean {
-    return !Utils.isUndefined(this.stationInfo.enableStatistics) ? this.stationInfo.enableStatistics : true;
+    this.autoReconnectRetryCount = 0;
+    this.hasSocketRestarted = false;
   }
 
-  _getNumberOfPhases(): number {
-    switch (this._getPowerOutType()) {
-      case PowerOutType.AC:
-        return !Utils.isUndefined(this.stationInfo.numberOfPhases) ? this.stationInfo.numberOfPhases : 3;
-      case PowerOutType.DC:
-        return 0;
+  private async onClose(closeEvent): Promise<void> {
+    switch (closeEvent) {
+      case WebSocketCloseEventStatusCode.CLOSE_NORMAL: // Normal close
+      case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
+        logger.info(`${this.logPrefix()} Socket normally closed with status '${Utils.getWebSocketCloseEventStatusString(closeEvent)}'`);
+        this.autoReconnectRetryCount = 0;
+        break;
+      default: // Abnormal close
+        logger.error(`${this.logPrefix()} Socket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(closeEvent)}'`);
+        await this.reconnect(closeEvent);
+        break;
     }
   }
 
-  _getNumberOfRunningTransactions(): number {
-    let trxCount = 0;
+  private async onMessage(messageEvent: MessageEvent): Promise<void> {
+    let [messageType, messageId, commandName, commandPayload, errorDetails]: IncomingRequest = [0, '', '' as IncomingRequestCommand, {}, {}];
+    let responseCallback: (payload?: Record<string, unknown> | string, requestPayload?: Record<string, unknown>) => void;
+    let rejectCallback: (error: OCPPError) => void;
+    let requestPayload: Record<string, unknown>;
+    let errMsg: string;
+    try {
+      // Parse the message
+      [messageType, messageId, commandName, commandPayload, errorDetails] = JSON.parse(messageEvent.toString()) as IncomingRequest;
+
+      // Check the Type of message
+      switch (messageType) {
+        // Incoming Message
+        case MessageType.CALL_MESSAGE:
+          if (this.getEnableStatistics()) {
+            this.statistics.addMessage(commandName, messageType);
+          }
+          // Process the call
+          await this.ocppIncomingRequestService.handleRequest(messageId, commandName, commandPayload);
+          break;
+        // Outcome Message
+        case MessageType.CALL_RESULT_MESSAGE:
+          // Respond
+          if (Utils.isIterable(this.requests[messageId])) {
+            [responseCallback, , requestPayload] = this.requests[messageId];
+          } else {
+            throw new Error(`Response request for message id ${messageId} is not iterable`);
+          }
+          if (!responseCallback) {
+            // Error
+            throw new Error(`Response request for unknown message id ${messageId}`);
+          }
+          delete this.requests[messageId];
+          responseCallback(commandName, requestPayload);
+          break;
+        // Error Message
+        case MessageType.CALL_ERROR_MESSAGE:
+          if (!this.requests[messageId]) {
+            // Error
+            throw new Error(`Error request for unknown message id ${messageId}`);
+          }
+          if (Utils.isIterable(this.requests[messageId])) {
+            [, rejectCallback] = this.requests[messageId];
+          } else {
+            throw new Error(`Error request for message id ${messageId} is not iterable`);
+          }
+          delete this.requests[messageId];
+          rejectCallback(new OCPPError(commandName, commandPayload.toString(), errorDetails));
+          break;
+        // Error
+        default:
+          errMsg = `${this.logPrefix()} Wrong message type ${messageType}`;
+          logger.error(errMsg);
+          throw new Error(errMsg);
+      }
+    } catch (error) {
+      // Log
+      logger.error('%s Incoming message %j processing error %j on request content type %j', this.logPrefix(), messageEvent, error, this.requests[messageId]);
+      // Send error
+      messageType !== MessageType.CALL_ERROR_MESSAGE && await this.ocppRequestService.sendError(messageId, error, commandName);
+    }
+  }
+
+  private onPing(): void {
+    logger.debug(this.logPrefix() + ' Has received a WS ping (rfc6455) from the server');
+  }
+
+  private onPong(): void {
+    logger.debug(this.logPrefix() + ' Has received a WS pong (rfc6455) from the server');
+  }
+
+  private async onError(errorEvent): Promise<void> {
+    logger.error(this.logPrefix() + ' Socket error: %j', errorEvent);
+    // pragma switch (errorEvent.code) {
+    //   case 'ECONNREFUSED':
+    //     await this._reconnect(errorEvent);
+    //     break;
+    // }
+  }
+
+  private getTemplateChargingStationConfiguration(): ChargingStationConfiguration {
+    return this.stationInfo.Configuration ? this.stationInfo.Configuration : {} as ChargingStationConfiguration;
+  }
+
+  private getAuthorizationFile(): string {
+    return this.stationInfo.authorizationFile && this.stationInfo.authorizationFile;
+  }
+
+  private getAuthorizedTags(): string[] {
+    let authorizedTags: string[] = [];
+    const authorizationFile = this.getAuthorizationFile();
+    if (authorizationFile) {
+      try {
+        // Load authorization file
+        const fileDescriptor = fs.openSync(authorizationFile, 'r');
+        authorizedTags = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8')) as string[];
+        fs.closeSync(fileDescriptor);
+      } catch (error) {
+        logger.error(this.logPrefix() + ' Authorization file ' + authorizationFile + ' loading error: %j', error);
+        throw error;
+      }
+    } else {
+      logger.info(this.logPrefix() + ' No authorization file given in template file ' + this.stationTemplateFile);
+    }
+    return authorizedTags;
+  }
+
+  private getUseConnectorId0(): boolean {
+    return !Utils.isUndefined(this.stationInfo.useConnectorId0) ? this.stationInfo.useConnectorId0 : true;
+  }
+
+  private getNumberOfRunningTransactions(): number {
+    let trxCount = 0;
     for (const connector in this.connectors) {
       if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector)).transactionStarted) {
         trxCount++;
@@ -256,7 +614,7 @@ export default class ChargingStation {
   }
 
   // 0 for disabling
-  _getConnectionTimeout(): number {
+  private getConnectionTimeout(): number {
     if (!Utils.isUndefined(this.stationInfo.connectionTimeout)) {
       return this.stationInfo.connectionTimeout;
     }
@@ -267,7 +625,7 @@ export default class ChargingStation {
   }
 
   // -1 for unlimited, 0 for disabling
-  _getAutoReconnectMaxRetries(): number {
+  private getAutoReconnectMaxRetries(): number {
     if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) {
       return this.stationInfo.autoReconnectMaxRetries;
     }
@@ -278,38 +636,26 @@ export default class ChargingStation {
   }
 
   // 0 for disabling
-  _getRegistrationMaxRetries(): number {
+  private getRegistrationMaxRetries(): number {
     if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) {
       return this.stationInfo.registrationMaxRetries;
     }
     return -1;
   }
 
-  _getPowerDivider(): number {
-    let powerDivider = this._getNumberOfConnectors();
+  private getPowerDivider(): number {
+    let powerDivider = this.getNumberOfConnectors();
     if (this.stationInfo.powerSharedByConnectors) {
-      powerDivider = this._getNumberOfRunningTransactions();
+      powerDivider = this.getNumberOfRunningTransactions();
     }
     return powerDivider;
   }
 
-  getConnector(id: number): Connector {
-    return this.connectors[id];
-  }
-
-  _isConnectorAvailable(id: number): boolean {
-    return this.getConnector(id).availability === AvailabilityType.OPERATIVE;
-  }
-
-  _isChargingStationAvailable(): boolean {
-    return this.getConnector(0).availability === AvailabilityType.OPERATIVE;
-  }
-
-  _getTemplateMaxNumberOfConnectors(): number {
+  private getTemplateMaxNumberOfConnectors(): number {
     return Object.keys(this.stationInfo.Connectors).length;
   }
 
-  _getMaxNumberOfConnectors(): number {
+  private getMaxNumberOfConnectors(): number {
     let maxConnectors = 0;
     if (!Utils.isEmptyArray(this.stationInfo.numberOfConnectors)) {
       const numberOfConnectors = this.stationInfo.numberOfConnectors as number[];
@@ -318,113 +664,39 @@ export default class ChargingStation {
     } else if (!Utils.isUndefined(this.stationInfo.numberOfConnectors)) {
       maxConnectors = this.stationInfo.numberOfConnectors as number;
     } else {
-      maxConnectors = this.stationInfo.Connectors[0] ? this._getTemplateMaxNumberOfConnectors() - 1 : this._getTemplateMaxNumberOfConnectors();
+      maxConnectors = this.stationInfo.Connectors[0] ? this.getTemplateMaxNumberOfConnectors() - 1 : this.getTemplateMaxNumberOfConnectors();
     }
     return maxConnectors;
   }
 
-  _getNumberOfConnectors(): number {
+  private getNumberOfConnectors(): number {
     return this.connectors[0] ? Object.keys(this.connectors).length - 1 : Object.keys(this.connectors).length;
   }
 
-  _getVoltageOut(): number {
-    const errMsg = `${this._logPrefix()} Unknown ${this._getPowerOutType()} powerOutType in template file ${this.stationTemplateFile}, cannot define default voltage out`;
-    let defaultVoltageOut: number;
-    switch (this._getPowerOutType()) {
-      case PowerOutType.AC:
-        defaultVoltageOut = VoltageOut.VOLTAGE_230;
-        break;
-      case PowerOutType.DC:
-        defaultVoltageOut = VoltageOut.VOLTAGE_400;
-        break;
-      default:
-        logger.error(errMsg);
-        throw Error(errMsg);
-    }
-    return !Utils.isUndefined(this.stationInfo.voltageOut) ? this.stationInfo.voltageOut : defaultVoltageOut;
-  }
-
-  _getTransactionIdTag(transactionId: number): string {
-    for (const connector in this.connectors) {
-      if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) {
-        return this.getConnector(Utils.convertToInt(connector)).idTag;
-      }
-    }
-  }
-
-  _getTransactionMeterStop(transactionId: number): number {
-    for (const connector in this.connectors) {
-      if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) {
-        return this.getConnector(Utils.convertToInt(connector)).lastEnergyActiveImportRegisterValue;
-      }
-    }
-  }
-
-  _getPowerOutType(): PowerOutType {
-    return !Utils.isUndefined(this.stationInfo.powerOutType) ? this.stationInfo.powerOutType : PowerOutType.AC;
-  }
-
-  _getSupervisionURL(): string {
-    const supervisionUrls = Utils.cloneObject<string | string[]>(this.stationInfo.supervisionURL ? this.stationInfo.supervisionURL : Configuration.getSupervisionURLs());
-    let indexUrl = 0;
-    if (!Utils.isEmptyArray(supervisionUrls)) {
-      if (Configuration.getDistributeStationsToTenantsEqually()) {
-        indexUrl = this.index % supervisionUrls.length;
-      } else {
-        // Get a random url
-        indexUrl = Math.floor(Math.random() * supervisionUrls.length);
-      }
-      return supervisionUrls[indexUrl];
-    }
-    return supervisionUrls as string;
-  }
-
-  _getReconnectExponentialDelay(): boolean {
-    return !Utils.isUndefined(this.stationInfo.reconnectExponentialDelay) ? this.stationInfo.reconnectExponentialDelay : false;
-  }
-
-  _getHeartbeatInterval(): number {
-    const HeartbeatInterval = this._getConfigurationKey(StandardParametersKey.HeartbeatInterval);
-    if (HeartbeatInterval) {
-      return Utils.convertToInt(HeartbeatInterval.value) * 1000;
-    }
-    const HeartBeatInterval = this._getConfigurationKey(StandardParametersKey.HeartBeatInterval);
-    if (HeartBeatInterval) {
-      return Utils.convertToInt(HeartBeatInterval.value) * 1000;
-    }
-  }
-
-  _getAuthorizeRemoteTxRequests(): boolean {
-    const authorizeRemoteTxRequests = this._getConfigurationKey(StandardParametersKey.AuthorizeRemoteTxRequests);
-    return authorizeRemoteTxRequests ? Utils.convertToBoolean(authorizeRemoteTxRequests.value) : false;
-  }
-
-  _getLocalAuthListEnabled(): boolean {
-    const localAuthListEnabled = this._getConfigurationKey(StandardParametersKey.LocalAuthListEnabled);
-    return localAuthListEnabled ? Utils.convertToBoolean(localAuthListEnabled.value) : false;
-  }
-
-  async _startMessageSequence(): Promise<void> {
+  private async startMessageSequence(): Promise<void> {
     // Start WebSocket ping
-    this._startWebSocketPing();
+    this.startWebSocketPing();
     // Start heartbeat
-    this._startHeartbeat();
+    this.startHeartbeat();
     // Initialize connectors status
     for (const connector in this.connectors) {
       if (Utils.convertToInt(connector) === 0) {
         continue;
       } else if (!this.hasStopped && !this.getConnector(Utils.convertToInt(connector))?.status && this.getConnector(Utils.convertToInt(connector))?.bootStatus) {
         // Send status in template at startup
-        await this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).bootStatus);
+        await this.ocppRequestService.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).bootStatus);
+        this.getConnector(Utils.convertToInt(connector)).status = this.getConnector(Utils.convertToInt(connector)).bootStatus;
       } else if (this.hasStopped && this.getConnector(Utils.convertToInt(connector))?.bootStatus) {
         // Send status in template after reset
-        await this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).bootStatus);
+        await this.ocppRequestService.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).bootStatus);
+        this.getConnector(Utils.convertToInt(connector)).status = this.getConnector(Utils.convertToInt(connector)).bootStatus;
       } else if (!this.hasStopped && this.getConnector(Utils.convertToInt(connector))?.status) {
         // Send previous status at template reload
-        await this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).status);
+        await this.ocppRequestService.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).status);
       } else {
         // Send default status
-        await this.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.AVAILABLE);
+        await this.ocppRequestService.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.AVAILABLE);
+        this.getConnector(Utils.convertToInt(connector)).status = ChargePointStatus.AVAILABLE;
       }
     }
     // Start the ATG
@@ -433,7 +705,7 @@ export default class ChargingStation {
         this.automaticTransactionGeneration = new AutomaticTransactionGenerator(this);
       }
       if (this.automaticTransactionGeneration.timeToStop) {
-        this.automaticTransactionGeneration.start();
+        await this.automaticTransactionGeneration.start();
       }
     }
     if (this.getEnableStatistics()) {
@@ -441,11 +713,11 @@ export default class ChargingStation {
     }
   }
 
-  async _stopMessageSequence(reason: StopTransactionReason = StopTransactionReason.NONE): Promise<void> {
+  private async stopMessageSequence(reason: StopTransactionReason = StopTransactionReason.NONE): Promise<void> {
     // Stop WebSocket ping
-    this._stopWebSocketPing();
+    this.stopWebSocketPing();
     // Stop heartbeat
-    this._stopHeartbeat();
+    this.stopHeartbeat();
     // Stop the ATG
     if (this.stationInfo.AutomaticTransactionGenerator.enable &&
       this.automaticTransactionGeneration &&
@@ -454,91 +726,115 @@ export default class ChargingStation {
     } else {
       for (const connector in this.connectors) {
         if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector)).transactionStarted) {
-          await this.sendStopTransaction(this.getConnector(Utils.convertToInt(connector)).transactionId, reason);
+          const transactionId = this.getConnector(Utils.convertToInt(connector)).transactionId;
+          await this.ocppRequestService.sendStopTransaction(transactionId, this.getTransactionMeterStop(transactionId), this.getTransactionIdTag(transactionId), reason);
         }
       }
     }
   }
 
-  _startWebSocketPing(): void {
-    const webSocketPingInterval: number = this._getConfigurationKey(StandardParametersKey.WebSocketPingInterval) ? Utils.convertToInt(this._getConfigurationKey(StandardParametersKey.WebSocketPingInterval).value) : 0;
+  private startWebSocketPing(): void {
+    const webSocketPingInterval: number = this.getConfigurationKey(StandardParametersKey.WebSocketPingInterval) ? Utils.convertToInt(this.getConfigurationKey(StandardParametersKey.WebSocketPingInterval).value) : 0;
     if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
       this.webSocketPingSetInterval = setInterval(() => {
-        if (this._isWebSocketOpen()) {
+        if (this.isWebSocketOpen()) {
           this.wsConnection.ping((): void => { });
         }
       }, webSocketPingInterval * 1000);
-      logger.info(this._logPrefix() + ' WebSocket ping started every ' + Utils.secondsToHHMMSS(webSocketPingInterval));
+      logger.info(this.logPrefix() + ' WebSocket ping started every ' + Utils.secondsToHHMMSS(webSocketPingInterval));
     } else if (this.webSocketPingSetInterval) {
-      logger.info(this._logPrefix() + ' WebSocket ping every ' + Utils.secondsToHHMMSS(webSocketPingInterval) + ' already started');
+      logger.info(this.logPrefix() + ' WebSocket ping every ' + Utils.secondsToHHMMSS(webSocketPingInterval) + ' already started');
     } else {
-      logger.error(`${this._logPrefix()} WebSocket ping interval set to ${webSocketPingInterval ? Utils.secondsToHHMMSS(webSocketPingInterval) : webSocketPingInterval}, not starting the WebSocket ping`);
+      logger.error(`${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval ? Utils.secondsToHHMMSS(webSocketPingInterval) : webSocketPingInterval}, not starting the WebSocket ping`);
     }
   }
 
-  _stopWebSocketPing(): void {
+  private stopWebSocketPing(): void {
     if (this.webSocketPingSetInterval) {
       clearInterval(this.webSocketPingSetInterval);
       this.webSocketPingSetInterval = null;
     }
   }
 
-  _restartWebSocketPing(): void {
-    // Stop WebSocket ping
-    this._stopWebSocketPing();
-    // Start WebSocket ping
-    this._startWebSocketPing();
+  private getSupervisionURL(): string {
+    const supervisionUrls = Utils.cloneObject<string | string[]>(this.stationInfo.supervisionURL ? this.stationInfo.supervisionURL : Configuration.getSupervisionURLs());
+    let indexUrl = 0;
+    if (!Utils.isEmptyArray(supervisionUrls)) {
+      if (Configuration.getDistributeStationsToTenantsEqually()) {
+        indexUrl = this.index % supervisionUrls.length;
+      } else {
+        // Get a random url
+        indexUrl = Math.floor(Math.random() * supervisionUrls.length);
+      }
+      return supervisionUrls[indexUrl];
+    }
+    return supervisionUrls as string;
   }
 
-  _startHeartbeat(): void {
-    if (this._getHeartbeatInterval() && this._getHeartbeatInterval() > 0 && !this.heartbeatSetInterval) {
-      this.heartbeatSetInterval = setInterval(async () => {
-        await this.sendHeartbeat();
-      }, this._getHeartbeatInterval());
-      logger.info(this._logPrefix() + ' Heartbeat started every ' + Utils.milliSecondsToHHMMSS(this._getHeartbeatInterval()));
-    } else if (this.heartbeatSetInterval) {
-      logger.info(this._logPrefix() + ' Heartbeat every ' + Utils.milliSecondsToHHMMSS(this._getHeartbeatInterval()) + ' already started');
-    } else {
-      logger.error(`${this._logPrefix()} Heartbeat interval set to ${this._getHeartbeatInterval() ? Utils.milliSecondsToHHMMSS(this._getHeartbeatInterval()) : this._getHeartbeatInterval()}, not starting the heartbeat`);
+  private getHeartbeatInterval(): number {
+    const HeartbeatInterval = this.getConfigurationKey(StandardParametersKey.HeartbeatInterval);
+    if (HeartbeatInterval) {
+      return Utils.convertToInt(HeartbeatInterval.value) * 1000;
+    }
+    const HeartBeatInterval = this.getConfigurationKey(StandardParametersKey.HeartBeatInterval);
+    if (HeartBeatInterval) {
+      return Utils.convertToInt(HeartBeatInterval.value) * 1000;
     }
   }
 
-  _stopHeartbeat(): void {
+  private stopHeartbeat(): void {
     if (this.heartbeatSetInterval) {
       clearInterval(this.heartbeatSetInterval);
       this.heartbeatSetInterval = null;
     }
   }
 
-  _restartHeartbeat(): void {
-    // Stop heartbeat
-    this._stopHeartbeat();
-    // Start heartbeat
-    this._startHeartbeat();
+  private openWSConnection(options?: WebSocket.ClientOptions, forceCloseOpened = false): void {
+    if (Utils.isUndefined(options)) {
+      options = {} as WebSocket.ClientOptions;
+    }
+    if (Utils.isUndefined(options.handshakeTimeout)) {
+      options.handshakeTimeout = this.getConnectionTimeout() * 1000;
+    }
+    if (this.isWebSocketOpen() && forceCloseOpened) {
+      this.wsConnection.close();
+    }
+    let protocol;
+    switch (this.getOCPPVersion()) {
+      case OCPPVersion.VERSION_16:
+        protocol = 'ocpp' + OCPPVersion.VERSION_16;
+        break;
+      default:
+        this.handleUnsupportedVersion(this.getOCPPVersion());
+        break;
+    }
+    this.wsConnection = new WebSocket(this.wsConnectionUrl, protocol, options);
+    logger.info(this.logPrefix() + ' Will communicate through URL ' + this.supervisionUrl);
   }
 
-  _startAuthorizationFileMonitoring(): void {
-    fs.watch(this._getAuthorizationFile()).on('change', (e) => {
+  private startAuthorizationFileMonitoring(): void {
+    fs.watch(this.getAuthorizationFile()).on('change', (e) => {
       try {
-        logger.debug(this._logPrefix() + ' Authorization file ' + this._getAuthorizationFile() + ' have changed, reload');
-        // Initialize _authorizedTags
-        this.authorizedTags = this._loadAndGetAuthorizedTags();
+        logger.debug(this.logPrefix() + ' Authorization file ' + this.getAuthorizationFile() + ' have changed, reload');
+        // Initialize authorizedTags
+        this.authorizedTags = this.getAuthorizedTags();
       } catch (error) {
-        logger.error(this._logPrefix() + ' Authorization file monitoring error: %j', error);
+        logger.error(this.logPrefix() + ' Authorization file monitoring error: %j', error);
       }
     });
   }
 
-  _startStationTemplateFileMonitoring(): void {
-    fs.watch(this.stationTemplateFile).on('change', (e) => {
+  private startStationTemplateFileMonitoring(): void {
+    // eslint-disable-next-line @typescript-eslint/no-misused-promises
+    fs.watch(this.stationTemplateFile).on('change', async (e): Promise<void> => {
       try {
-        logger.debug(this._logPrefix() + ' Template file ' + this.stationTemplateFile + ' have changed, reload');
+        logger.debug(this.logPrefix() + ' Template file ' + this.stationTemplateFile + ' have changed, reload');
         // Initialize
-        this._initialize();
+        this.initialize();
         // Stop the ATG
         if (!this.stationInfo.AutomaticTransactionGenerator.enable &&
           this.automaticTransactionGeneration) {
-          this.automaticTransactionGeneration.stop().catch(() => { });
+          await this.automaticTransactionGeneration.stop();
         }
         // Start the ATG
         if (this.stationInfo.AutomaticTransactionGenerator.enable) {
@@ -546,1069 +842,48 @@ export default class ChargingStation {
             this.automaticTransactionGeneration = new AutomaticTransactionGenerator(this);
           }
           if (this.automaticTransactionGeneration.timeToStop) {
-            this.automaticTransactionGeneration.start();
+            await this.automaticTransactionGeneration.start();
           }
         }
         // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
       } catch (error) {
-        logger.error(this._logPrefix() + ' Charging station template file monitoring error: %j', error);
+        logger.error(this.logPrefix() + ' Charging station template file monitoring error: %j', error);
       }
     });
   }
 
-  _startMeterValues(connectorId: number, interval: number): void {
-    if (connectorId === 0) {
-      logger.error(`${this._logPrefix()} Trying to start MeterValues on connector Id ${connectorId.toString()}`);
-      return;
-    }
-    if (!this.getConnector(connectorId)) {
-      logger.error(`${this._logPrefix()} Trying to start MeterValues on non existing connector Id ${connectorId.toString()}`);
-      return;
-    }
-    if (!this.getConnector(connectorId)?.transactionStarted) {
-      logger.error(`${this._logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction started`);
-      return;
-    } else if (this.getConnector(connectorId)?.transactionStarted && !this.getConnector(connectorId)?.transactionId) {
-      logger.error(`${this._logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction id`);
-      return;
-    }
-    if (interval > 0) {
-      this.getConnector(connectorId).transactionSetInterval = setInterval(async () => {
-        if (this.getEnableStatistics()) {
-          const sendMeterValues = performance.timerify(this.sendMeterValues);
-          this.performanceObserver.observe({
-            entryTypes: ['function'],
-          });
-          await sendMeterValues(connectorId, interval, this);
-        } else {
-          await this.sendMeterValues(connectorId, interval, this);
-        }
-      }, interval);
-    } else {
-      logger.error(`${this._logPrefix()} Charging station ${StandardParametersKey.MeterValueSampleInterval} configuration set to ${Utils.milliSecondsToHHMMSS(interval)}, not sending MeterValues`);
-    }
+  private getReconnectExponentialDelay(): boolean {
+    return !Utils.isUndefined(this.stationInfo.reconnectExponentialDelay) ? this.stationInfo.reconnectExponentialDelay : false;
   }
 
-  _openWSConnection(options?: WebSocket.ClientOptions, forceCloseOpened = false): void {
-    if (Utils.isUndefined(options)) {
-      options = {} as WebSocket.ClientOptions;
-    }
-    if (Utils.isUndefined(options.handshakeTimeout)) {
-      options.handshakeTimeout = this._getConnectionTimeout() * 1000;
+  private async reconnect(error): Promise<void> {
+    // Stop heartbeat
+    this.stopHeartbeat();
+    // Stop the ATG if needed
+    if (this.stationInfo.AutomaticTransactionGenerator.enable &&
+      this.stationInfo.AutomaticTransactionGenerator.stopOnConnectionFailure &&
+      this.automaticTransactionGeneration &&
+      !this.automaticTransactionGeneration.timeToStop) {
+      this.automaticTransactionGeneration.stop().catch(() => { });
     }
-    if (this._isWebSocketOpen() && forceCloseOpened) {
-      this.wsConnection.close();
+    if (this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() || this.getAutoReconnectMaxRetries() === -1) {
+      this.autoReconnectRetryCount++;
+      const reconnectDelay = (this.getReconnectExponentialDelay() ? Utils.exponentialDelay(this.autoReconnectRetryCount) : this.getConnectionTimeout() * 1000);
+      logger.error(`${this.logPrefix()} Socket: connection retry in ${Utils.roundTo(reconnectDelay, 2)}ms, timeout ${reconnectDelay - 100}ms`);
+      await Utils.sleep(reconnectDelay);
+      logger.error(this.logPrefix() + ' Socket: reconnecting try #' + this.autoReconnectRetryCount.toString());
+      this.openWSConnection({ handshakeTimeout: reconnectDelay - 100 });
+      this.hasSocketRestarted = true;
+    } else if (this.getAutoReconnectMaxRetries() !== -1) {
+      logger.error(`${this.logPrefix()} Socket reconnect failure: max retries reached (${this.autoReconnectRetryCount}) or retry disabled (${this.getAutoReconnectMaxRetries()})`);
     }
-    this.wsConnection = new WebSocket(this.wsConnectionUrl, 'ocpp' + Constants.OCPP_VERSION_16, options);
-    logger.info(this._logPrefix() + ' Will communicate through URL ' + this.supervisionUrl);
-  }
-
-  start(): void {
-    this._openWSConnection();
-    // Monitor authorization file
-    this._startAuthorizationFileMonitoring();
-    // Monitor station template file
-    this._startStationTemplateFileMonitoring();
-    // Handle Socket incoming messages
-    this.wsConnection.on('message', this.onMessage.bind(this));
-    // Handle Socket error
-    this.wsConnection.on('error', this.onError.bind(this));
-    // Handle Socket close
-    this.wsConnection.on('close', this.onClose.bind(this));
-    // Handle Socket opening connection
-    this.wsConnection.on('open', this.onOpen.bind(this));
-    // Handle Socket ping
-    this.wsConnection.on('ping', this.onPing.bind(this));
-    // Handle Socket pong
-    this.wsConnection.on('pong', this.onPong.bind(this));
   }
 
-  async stop(reason: StopTransactionReason = StopTransactionReason.NONE): Promise<void> {
-    // Stop message sequence
-    await this._stopMessageSequence(reason);
-    for (const connector in this.connectors) {
-      if (Utils.convertToInt(connector) > 0) {
-        await this.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.UNAVAILABLE);
-      }
-    }
-    if (this._isWebSocketOpen()) {
-      this.wsConnection.close();
-    }
-    this.bootNotificationResponse = null;
-    this.hasStopped = true;
-  }
-
-  async _reconnect(error): Promise<void> {
-    // Stop heartbeat
-    this._stopHeartbeat();
-    // Stop the ATG if needed
-    if (this.stationInfo.AutomaticTransactionGenerator.enable &&
-      this.stationInfo.AutomaticTransactionGenerator.stopOnConnectionFailure &&
-      this.automaticTransactionGeneration &&
-      !this.automaticTransactionGeneration.timeToStop) {
-      this.automaticTransactionGeneration.stop().catch(() => { });
-    }
-    if (this.autoReconnectRetryCount < this._getAutoReconnectMaxRetries() || this._getAutoReconnectMaxRetries() === -1) {
-      this.autoReconnectRetryCount++;
-      const reconnectDelay = (this._getReconnectExponentialDelay() ? Utils.exponentialDelay(this.autoReconnectRetryCount) : this._getConnectionTimeout() * 1000);
-      logger.error(`${this._logPrefix()} Socket: connection retry in ${Utils.roundTo(reconnectDelay, 2)}ms, timeout ${reconnectDelay - 100}ms`);
-      await Utils.sleep(reconnectDelay);
-      logger.error(this._logPrefix() + ' Socket: reconnecting try #' + this.autoReconnectRetryCount.toString());
-      this._openWSConnection({ handshakeTimeout: reconnectDelay - 100 });
-      this.hasSocketRestarted = true;
-    } else if (this._getAutoReconnectMaxRetries() !== -1) {
-      logger.error(`${this._logPrefix()} Socket reconnect failure: max retries reached (${this.autoReconnectRetryCount}) or retry disabled (${this._getAutoReconnectMaxRetries()})`);
-    }
-  }
-
-  async onOpen(): Promise<void> {
-    logger.info(`${this._logPrefix()} Is connected to server through ${this.wsConnectionUrl}`);
-    if (!this._isRegistered()) {
-      // Send BootNotification
-      let registrationRetryCount = 0;
-      do {
-        this.bootNotificationResponse = await this.sendBootNotification();
-        if (!this._isRegistered()) {
-          registrationRetryCount++;
-          await Utils.sleep(this.bootNotificationResponse?.interval ? this.bootNotificationResponse.interval * 1000 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL);
-        }
-      } while (!this._isRegistered() && (registrationRetryCount <= this._getRegistrationMaxRetries() || this._getRegistrationMaxRetries() === -1));
-    }
-    if (this._isRegistered()) {
-      await this._startMessageSequence();
-      if (this.hasSocketRestarted && this._isWebSocketOpen()) {
-        if (!Utils.isEmptyArray(this.messageQueue)) {
-          this.messageQueue.forEach((message, index) => {
-            this.messageQueue.splice(index, 1);
-            this.wsConnection.send(message);
-          });
-        }
-      }
-    } else {
-      logger.error(`${this._logPrefix()} Registration failure: max retries reached (${this._getRegistrationMaxRetries()}) or retry disabled (${this._getRegistrationMaxRetries()})`);
-    }
-    this.autoReconnectRetryCount = 0;
-    this.hasSocketRestarted = false;
-  }
-
-  async onError(errorEvent): Promise<void> {
-    logger.error(this._logPrefix() + ' Socket error: %j', errorEvent);
-    // pragma switch (errorEvent.code) {
-    //   case 'ECONNREFUSED':
-    //     await this._reconnect(errorEvent);
-    //     break;
-    // }
-  }
-
-  async onClose(closeEvent): Promise<void> {
-    switch (closeEvent) {
-      case WebSocketCloseEventStatusCode.CLOSE_NORMAL: // Normal close
-      case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
-        logger.info(`${this._logPrefix()} Socket normally closed with status '${Utils.getWebSocketCloseEventStatusString(closeEvent)}'`);
-        this.autoReconnectRetryCount = 0;
-        break;
-      default: // Abnormal close
-        logger.error(`${this._logPrefix()} Socket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(closeEvent)}'`);
-        await this._reconnect(closeEvent);
-        break;
-    }
-  }
-
-  onPing(): void {
-    logger.debug(this._logPrefix() + ' Has received a WS ping (rfc6455) from the server');
-  }
-
-  onPong(): void {
-    logger.debug(this._logPrefix() + ' Has received a WS pong (rfc6455) from the server');
-  }
-
-  async onMessage(messageEvent: MessageEvent): Promise<void> {
-    let [messageType, messageId, commandName, commandPayload, errorDetails]: IncomingRequest = [0, '', '' as IncomingRequestCommand, {}, {}];
-    let responseCallback: (payload?: Record<string, unknown> | string, requestPayload?: Record<string, unknown>) => void;
-    let rejectCallback: (error: OCPPError) => void;
-    let requestPayload: Record<string, unknown>;
-    let errMsg: string;
-    try {
-      // Parse the message
-      [messageType, messageId, commandName, commandPayload, errorDetails] = JSON.parse(messageEvent.toString()) as IncomingRequest;
-
-      // Check the Type of message
-      switch (messageType) {
-        // Incoming Message
-        case MessageType.CALL_MESSAGE:
-          if (this.getEnableStatistics()) {
-            this.statistics.addMessage(commandName, messageType);
-          }
-          // Process the call
-          await this.handleRequest(messageId, commandName, commandPayload);
-          break;
-        // Outcome Message
-        case MessageType.CALL_RESULT_MESSAGE:
-          // Respond
-          if (Utils.isIterable(this.requests[messageId])) {
-            [responseCallback, , requestPayload] = this.requests[messageId];
-          } else {
-            throw new Error(`Response request for message id ${messageId} is not iterable`);
-          }
-          if (!responseCallback) {
-            // Error
-            throw new Error(`Response request for unknown message id ${messageId}`);
-          }
-          delete this.requests[messageId];
-          responseCallback(commandName, requestPayload);
-          break;
-        // Error Message
-        case MessageType.CALL_ERROR_MESSAGE:
-          if (!this.requests[messageId]) {
-            // Error
-            throw new Error(`Error request for unknown message id ${messageId}`);
-          }
-          if (Utils.isIterable(this.requests[messageId])) {
-            [, rejectCallback] = this.requests[messageId];
-          } else {
-            throw new Error(`Error request for message id ${messageId} is not iterable`);
-          }
-          delete this.requests[messageId];
-          rejectCallback(new OCPPError(commandName, commandPayload.toString(), errorDetails));
-          break;
-        // Error
-        default:
-          errMsg = `${this._logPrefix()} Wrong message type ${messageType}`;
-          logger.error(errMsg);
-          throw new Error(errMsg);
-      }
-    } catch (error) {
-      // Log
-      logger.error('%s Incoming message %j processing error %j on request content type %j', this._logPrefix(), messageEvent, error, this.requests[messageId]);
-      // Send error
-      messageType !== MessageType.CALL_ERROR_MESSAGE && await this.sendError(messageId, error, commandName);
-    }
-  }
-
-  async sendHeartbeat(): Promise<void> {
-    try {
-      const payload: HeartbeatRequest = {};
-      await this.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, RequestCommand.HEARTBEAT);
-    } catch (error) {
-      this.handleRequestError(RequestCommand.HEARTBEAT, error);
-    }
-  }
-
-  async sendBootNotification(): Promise<BootNotificationResponse> {
-    try {
-      return await this.sendMessage(Utils.generateUUID(), this.bootNotificationRequest, MessageType.CALL_MESSAGE, RequestCommand.BOOT_NOTIFICATION) as BootNotificationResponse;
-    } catch (error) {
-      this.handleRequestError(RequestCommand.BOOT_NOTIFICATION, error);
-    }
-  }
-
-  async sendStatusNotification(connectorId: number, status: ChargePointStatus, errorCode: ChargePointErrorCode = ChargePointErrorCode.NO_ERROR): Promise<void> {
-    this.getConnector(connectorId).status = status;
-    try {
-      const payload: StatusNotificationRequest = {
-        connectorId,
-        errorCode,
-        status,
-      };
-      await this.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, RequestCommand.STATUS_NOTIFICATION);
-    } catch (error) {
-      this.handleRequestError(RequestCommand.STATUS_NOTIFICATION, error);
-    }
-  }
-
-  async sendAuthorize(idTag?: string): Promise<AuthorizeResponse> {
-    try {
-      const payload: AuthorizeRequest = {
-        ...!Utils.isUndefined(idTag) ? { idTag } : { idTag: Constants.TRANSACTION_DEFAULT_TAGID },
-      };
-      return await this.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, RequestCommand.AUTHORIZE) as AuthorizeResponse;
-    } catch (error) {
-      this.handleRequestError(RequestCommand.AUTHORIZE, error);
-    }
-  }
-
-  async sendStartTransaction(connectorId: number, idTag?: string): Promise<StartTransactionResponse> {
-    try {
-      const payload: StartTransactionRequest = {
-        connectorId,
-        ...!Utils.isUndefined(idTag) ? { idTag } : { idTag: Constants.TRANSACTION_DEFAULT_TAGID },
-        meterStart: 0,
-        timestamp: new Date().toISOString(),
-      };
-      return await this.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, RequestCommand.START_TRANSACTION) as StartTransactionResponse;
-    } catch (error) {
-      this.handleRequestError(RequestCommand.START_TRANSACTION, error);
-    }
-  }
-
-  async sendStopTransaction(transactionId: number, reason: StopTransactionReason = StopTransactionReason.NONE): Promise<StopTransactionResponse> {
-    const idTag = this._getTransactionIdTag(transactionId);
-    try {
-      const payload: StopTransactionRequest = {
-        transactionId,
-        ...!Utils.isUndefined(idTag) && { idTag: idTag },
-        meterStop: this._getTransactionMeterStop(transactionId),
-        timestamp: new Date().toISOString(),
-        ...reason && { reason },
-      };
-      return await this.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, RequestCommand.STOP_TRANSACTION) as StartTransactionResponse;
-    } catch (error) {
-      this.handleRequestError(RequestCommand.STOP_TRANSACTION, error);
-    }
-  }
-
-  async sendError(messageId: string, error: OCPPError, commandName: RequestCommand | IncomingRequestCommand): Promise<unknown> {
-    // Send error
-    return this.sendMessage(messageId, error, MessageType.CALL_ERROR_MESSAGE, commandName);
-  }
-
-  async sendMessage(messageId: string, commandParams: any, messageType: MessageType = MessageType.CALL_RESULT_MESSAGE, commandName: RequestCommand | IncomingRequestCommand): Promise<any> {
-    // eslint-disable-next-line @typescript-eslint/no-this-alias
-    const self = this;
-    // Send a message through wsConnection
-    return new Promise((resolve: (value?: any | PromiseLike<any>) => void, reject: (reason?: any) => void) => {
-      let messageToSend: string;
-      // Type of message
-      switch (messageType) {
-        // Request
-        case MessageType.CALL_MESSAGE:
-          // Build request
-          this.requests[messageId] = [responseCallback, rejectCallback, commandParams] as Request;
-          messageToSend = JSON.stringify([messageType, messageId, commandName, commandParams]);
-          break;
-        // Response
-        case MessageType.CALL_RESULT_MESSAGE:
-          // Build response
-          messageToSend = JSON.stringify([messageType, messageId, commandParams]);
-          break;
-        // Error Message
-        case MessageType.CALL_ERROR_MESSAGE:
-          // Build Error Message
-          messageToSend = JSON.stringify([messageType, messageId, commandParams.code ? commandParams.code : ErrorType.GENERIC_ERROR, commandParams.message ? commandParams.message : '', commandParams.details ? commandParams.details : {}]);
-          break;
-      }
-      // Check if wsConnection opened and charging station registered
-      if (this._isWebSocketOpen() && (this._isRegistered() || commandName === RequestCommand.BOOT_NOTIFICATION)) {
-        if (this.getEnableStatistics()) {
-          this.statistics.addMessage(commandName, messageType);
-        }
-        // Yes: Send Message
-        this.wsConnection.send(messageToSend);
-      } else if (commandName !== RequestCommand.BOOT_NOTIFICATION) {
-        let dups = false;
-        // Handle dups in buffer
-        for (const message of this.messageQueue) {
-          // Same message
-          if (messageToSend === message) {
-            dups = true;
-            break;
-          }
-        }
-        if (!dups) {
-          // Buffer message
-          this.messageQueue.push(messageToSend);
-        }
-        // Reject it
-        return rejectCallback(new OCPPError(commandParams.code ? commandParams.code : ErrorType.GENERIC_ERROR, commandParams.message ? commandParams.message : `WebSocket closed for message id '${messageId}' with content '${messageToSend}', message buffered`, commandParams.details ? commandParams.details : {}));
-      }
-      // Response?
-      if (messageType === MessageType.CALL_RESULT_MESSAGE) {
-        // Yes: send Ok
-        resolve();
-      } else if (messageType === MessageType.CALL_ERROR_MESSAGE) {
-        // Send timeout
-        setTimeout(() => rejectCallback(new OCPPError(commandParams.code ? commandParams.code : ErrorType.GENERIC_ERROR, commandParams.message ? commandParams.message : `Timeout for message id '${messageId}' with content '${messageToSend}'`, commandParams.details ? commandParams.details : {})), Constants.OCPP_ERROR_TIMEOUT);
-      }
-
-      // Function that will receive the request's response
-      async function responseCallback(payload: Record<string, unknown> | string, requestPayload: Record<string, unknown>): Promise<void> {
-        if (self.getEnableStatistics()) {
-          self.statistics.addMessage(commandName, messageType);
-        }
-        // Send the response
-        await self.handleResponse(commandName as RequestCommand, payload, requestPayload);
-        resolve(payload);
-      }
-
-      // Function that will receive the request's rejection
-      function rejectCallback(error: OCPPError): void {
-        if (self.getEnableStatistics()) {
-          self.statistics.addMessage(commandName, messageType);
-        }
-        logger.debug(`${self._logPrefix()} Error: %j occurred when calling command %s with parameters: %j`, error, commandName, commandParams);
-        // Build Exception
-        // eslint-disable-next-line no-empty-function
-        self.requests[messageId] = [() => { }, () => { }, {}]; // Properly format the request
-        // Send error
-        reject(error);
-      }
-    });
-  }
-
-  async handleResponse(commandName: RequestCommand, payload: Record<string, unknown> | string, requestPayload: Record<string, unknown>): Promise<void> {
-    const responseCallbackFn = 'handleResponse' + commandName;
-    if (typeof this[responseCallbackFn] === 'function') {
-      await this[responseCallbackFn](payload, requestPayload);
-    } else {
-      logger.error(this._logPrefix() + ' Trying to call an undefined response callback function: ' + responseCallbackFn);
-    }
-  }
-
-  handleResponseBootNotification(payload: BootNotificationResponse, requestPayload: BootNotificationRequest): void {
-    if (payload.status === RegistrationStatus.ACCEPTED) {
-      this.heartbeatSetInterval ? this._restartHeartbeat() : this._startHeartbeat();
-      this._addConfigurationKey(StandardParametersKey.HeartBeatInterval, payload.interval.toString());
-      this._addConfigurationKey(StandardParametersKey.HeartbeatInterval, payload.interval.toString(), false, false);
-      this.hasStopped && (this.hasStopped = false);
-    } else if (payload.status === RegistrationStatus.PENDING) {
-      logger.info(this._logPrefix() + ' Charging station in pending state on the central server');
-    } else {
-      logger.info(this._logPrefix() + ' Charging station rejected by the central server');
-    }
-  }
-
-  _initTransactionOnConnector(connectorId: number): void {
+  private initTransactionOnConnector(connectorId: number): void {
     this.getConnector(connectorId).transactionStarted = false;
     this.getConnector(connectorId).transactionId = null;
     this.getConnector(connectorId).idTag = null;
     this.getConnector(connectorId).lastEnergyActiveImportRegisterValue = -1;
   }
-
-  _resetTransactionOnConnector(connectorId: number): void {
-    this._initTransactionOnConnector(connectorId);
-    if (this.getConnector(connectorId)?.transactionSetInterval) {
-      clearInterval(this.getConnector(connectorId).transactionSetInterval);
-    }
-  }
-
-  async handleResponseStartTransaction(payload: StartTransactionResponse, requestPayload: StartTransactionRequest): Promise<void> {
-    const connectorId = requestPayload.connectorId;
-
-    let transactionConnectorId: number;
-    for (const connector in this.connectors) {
-      if (Utils.convertToInt(connector) > 0 && Utils.convertToInt(connector) === connectorId) {
-        transactionConnectorId = Utils.convertToInt(connector);
-        break;
-      }
-    }
-    if (!transactionConnectorId) {
-      logger.error(this._logPrefix() + ' Trying to start a transaction on a non existing connector Id ' + connectorId.toString());
-      return;
-    }
-    if (this.getConnector(connectorId)?.transactionStarted) {
-      logger.debug(this._logPrefix() + ' Trying to start a transaction on an already used connector ' + connectorId.toString() + ': %j', this.getConnector(connectorId));
-      return;
-    }
-
-    if (payload?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
-      this.getConnector(connectorId).transactionStarted = true;
-      this.getConnector(connectorId).transactionId = payload.transactionId;
-      this.getConnector(connectorId).idTag = requestPayload.idTag;
-      this.getConnector(connectorId).lastEnergyActiveImportRegisterValue = 0;
-      await this.sendStatusNotification(connectorId, ChargePointStatus.CHARGING);
-      logger.info(this._logPrefix() + ' Transaction ' + payload.transactionId.toString() + ' STARTED on ' + this.stationInfo.chargingStationId + '#' + connectorId.toString() + ' for idTag ' + requestPayload.idTag);
-      if (this.stationInfo.powerSharedByConnectors) {
-        this.stationInfo.powerDivider++;
-      }
-      const configuredMeterValueSampleInterval = this._getConfigurationKey(StandardParametersKey.MeterValueSampleInterval);
-      this._startMeterValues(connectorId,
-        configuredMeterValueSampleInterval ? Utils.convertToInt(configuredMeterValueSampleInterval.value) * 1000 : 60000);
-    } else {
-      logger.error(this._logPrefix() + ' Starting transaction id ' + payload.transactionId.toString() + ' REJECTED with status ' + payload?.idTagInfo?.status + ', idTag ' + requestPayload.idTag);
-      this._resetTransactionOnConnector(connectorId);
-      await this.sendStatusNotification(connectorId, ChargePointStatus.AVAILABLE);
-    }
-  }
-
-  async handleResponseStopTransaction(payload: StopTransactionResponse, requestPayload: StopTransactionRequest): Promise<void> {
-    let transactionConnectorId: number;
-    for (const connector in this.connectors) {
-      if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector))?.transactionId === requestPayload.transactionId) {
-        transactionConnectorId = Utils.convertToInt(connector);
-        break;
-      }
-    }
-    if (!transactionConnectorId) {
-      logger.error(this._logPrefix() + ' Trying to stop a non existing transaction ' + requestPayload.transactionId.toString());
-      return;
-    }
-    if (payload.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
-      if (!this._isChargingStationAvailable() || !this._isConnectorAvailable(transactionConnectorId)) {
-        await this.sendStatusNotification(transactionConnectorId, ChargePointStatus.UNAVAILABLE);
-      } else {
-        await this.sendStatusNotification(transactionConnectorId, ChargePointStatus.AVAILABLE);
-      }
-      if (this.stationInfo.powerSharedByConnectors) {
-        this.stationInfo.powerDivider--;
-      }
-      logger.info(this._logPrefix() + ' Transaction ' + requestPayload.transactionId.toString() + ' STOPPED on ' + this.stationInfo.chargingStationId + '#' + transactionConnectorId.toString());
-      this._resetTransactionOnConnector(transactionConnectorId);
-    } else {
-      logger.error(this._logPrefix() + ' Stopping transaction id ' + requestPayload.transactionId.toString() + ' REJECTED with status ' + payload.idTagInfo?.status);
-    }
-  }
-
-  handleResponseStatusNotification(payload: StatusNotificationRequest, requestPayload: StatusNotificationResponse): void {
-    logger.debug(this._logPrefix() + ' Status notification response received: %j to StatusNotification request: %j', payload, requestPayload);
-  }
-
-  handleResponseMeterValues(payload: MeterValuesRequest, requestPayload: MeterValuesResponse): void {
-    logger.debug(this._logPrefix() + ' MeterValues response received: %j to MeterValues request: %j', payload, requestPayload);
-  }
-
-  handleResponseHeartbeat(payload: HeartbeatResponse, requestPayload: HeartbeatRequest): void {
-    logger.debug(this._logPrefix() + ' Heartbeat response received: %j to Heartbeat request: %j', payload, requestPayload);
-  }
-
-  handleResponseAuthorize(payload: AuthorizeResponse, requestPayload: AuthorizeRequest): void {
-    logger.debug(this._logPrefix() + ' Authorize response received: %j to Authorize request: %j', payload, requestPayload);
-  }
-
-  async handleRequest(messageId: string, commandName: IncomingRequestCommand, commandPayload: Record<string, unknown>): Promise<void> {
-    let response;
-    // Call
-    if (typeof this['handleRequest' + commandName] === 'function') {
-      try {
-        // Call the method to build the response
-        response = await this['handleRequest' + commandName](commandPayload);
-      } catch (error) {
-        // Log
-        logger.error(this._logPrefix() + ' Handle request error: %j', error);
-        // Send back response to inform backend
-        await this.sendError(messageId, error, commandName);
-        throw error;
-      }
-    } else {
-      // Throw exception
-      await this.sendError(messageId, new OCPPError(ErrorType.NOT_IMPLEMENTED, `${commandName} is not implemented`, {}), commandName);
-      throw new Error(`${commandName} is not implemented ${JSON.stringify(commandPayload, null, ' ')}`);
-    }
-    // Send response
-    await this.sendMessage(messageId, response, MessageType.CALL_RESULT_MESSAGE, commandName);
-  }
-
-  // Simulate charging station restart
-  handleRequestReset(commandPayload: ResetRequest): DefaultResponse {
-    setImmediate(async () => {
-      await this.stop(commandPayload.type + 'Reset' as StopTransactionReason);
-      await Utils.sleep(this.stationInfo.resetTime);
-      await this.start();
-    });
-    logger.info(`${this._logPrefix()} ${commandPayload.type} reset command received, simulating it. The station will be back online in ${Utils.milliSecondsToHHMMSS(this.stationInfo.resetTime)}`);
-    return Constants.OCPP_RESPONSE_ACCEPTED;
-  }
-
-  handleRequestClearCache(): DefaultResponse {
-    return Constants.OCPP_RESPONSE_ACCEPTED;
-  }
-
-  async handleRequestUnlockConnector(commandPayload: UnlockConnectorRequest): Promise<UnlockConnectorResponse> {
-    const connectorId = commandPayload.connectorId;
-    if (connectorId === 0) {
-      logger.error(this._logPrefix() + ' Trying to unlock connector ' + connectorId.toString());
-      return Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED;
-    }
-    if (this.getConnector(connectorId)?.transactionStarted) {
-      const stopResponse = await this.sendStopTransaction(this.getConnector(connectorId).transactionId, StopTransactionReason.UNLOCK_COMMAND);
-      if (stopResponse.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
-        return Constants.OCPP_RESPONSE_UNLOCKED;
-      }
-      return Constants.OCPP_RESPONSE_UNLOCK_FAILED;
-    }
-    await this.sendStatusNotification(connectorId, ChargePointStatus.AVAILABLE);
-    return Constants.OCPP_RESPONSE_UNLOCKED;
-  }
-
-  _getConfigurationKey(key: string | StandardParametersKey, caseInsensitive = false): ConfigurationKey {
-    const configurationKey: ConfigurationKey = this.configuration.configurationKey.find((configElement) => {
-      if (caseInsensitive) {
-        return configElement.key.toLowerCase() === key.toLowerCase();
-      }
-      return configElement.key === key;
-    });
-    return configurationKey;
-  }
-
-  _addConfigurationKey(key: string | StandardParametersKey, value: string, readonly = false, visible = true, reboot = false): void {
-    const keyFound = this._getConfigurationKey(key);
-    if (!keyFound) {
-      this.configuration.configurationKey.push({
-        key,
-        readonly,
-        value,
-        visible,
-        reboot,
-      });
-    } else {
-      logger.error(`${this._logPrefix()} Trying to add an already existing configuration key: %j`, keyFound);
-    }
-  }
-
-  _setConfigurationKeyValue(key: string | StandardParametersKey, value: string): void {
-    const keyFound = this._getConfigurationKey(key);
-    if (keyFound) {
-      const keyIndex = this.configuration.configurationKey.indexOf(keyFound);
-      this.configuration.configurationKey[keyIndex].value = value;
-    } else {
-      logger.error(`${this._logPrefix()} Trying to set a value on a non existing configuration key: %j`, { key, value });
-    }
-  }
-
-  handleRequestGetConfiguration(commandPayload: GetConfigurationRequest): GetConfigurationResponse {
-    const configurationKey: OCPPConfigurationKey[] = [];
-    const unknownKey: string[] = [];
-    if (Utils.isEmptyArray(commandPayload.key)) {
-      for (const configuration of this.configuration.configurationKey) {
-        if (Utils.isUndefined(configuration.visible)) {
-          configuration.visible = true;
-        }
-        if (!configuration.visible) {
-          continue;
-        }
-        configurationKey.push({
-          key: configuration.key,
-          readonly: configuration.readonly,
-          value: configuration.value,
-        });
-      }
-    } else {
-      for (const key of commandPayload.key) {
-        const keyFound = this._getConfigurationKey(key);
-        if (keyFound) {
-          if (Utils.isUndefined(keyFound.visible)) {
-            keyFound.visible = true;
-          }
-          if (!keyFound.visible) {
-            continue;
-          }
-          configurationKey.push({
-            key: keyFound.key,
-            readonly: keyFound.readonly,
-            value: keyFound.value,
-          });
-        } else {
-          unknownKey.push(key);
-        }
-      }
-    }
-    return {
-      configurationKey,
-      unknownKey,
-    };
-  }
-
-  handleRequestChangeConfiguration(commandPayload: ChangeConfigurationRequest): ChangeConfigurationResponse {
-    // JSON request fields type sanity check
-    if (!Utils.isString(commandPayload.key)) {
-      logger.error(`${this._logPrefix()} ChangeConfiguration request key field is not a string:`, commandPayload);
-    }
-    if (!Utils.isString(commandPayload.value)) {
-      logger.error(`${this._logPrefix()} ChangeConfiguration request value field is not a string:`, commandPayload);
-    }
-    const keyToChange = this._getConfigurationKey(commandPayload.key, true);
-    if (!keyToChange) {
-      return Constants.OCPP_CONFIGURATION_RESPONSE_NOT_SUPPORTED;
-    } else if (keyToChange && keyToChange.readonly) {
-      return Constants.OCPP_CONFIGURATION_RESPONSE_REJECTED;
-    } else if (keyToChange && !keyToChange.readonly) {
-      const keyIndex = this.configuration.configurationKey.indexOf(keyToChange);
-      let valueChanged = false;
-      if (this.configuration.configurationKey[keyIndex].value !== commandPayload.value) {
-        this.configuration.configurationKey[keyIndex].value = commandPayload.value;
-        valueChanged = true;
-      }
-      let triggerHeartbeatRestart = false;
-      if (keyToChange.key === StandardParametersKey.HeartBeatInterval && valueChanged) {
-        this._setConfigurationKeyValue(StandardParametersKey.HeartbeatInterval, commandPayload.value);
-        triggerHeartbeatRestart = true;
-      }
-      if (keyToChange.key === StandardParametersKey.HeartbeatInterval && valueChanged) {
-        this._setConfigurationKeyValue(StandardParametersKey.HeartBeatInterval, commandPayload.value);
-        triggerHeartbeatRestart = true;
-      }
-      if (triggerHeartbeatRestart) {
-        this._restartHeartbeat();
-      }
-      if (keyToChange.key === StandardParametersKey.WebSocketPingInterval && valueChanged) {
-        this._restartWebSocketPing();
-      }
-      if (keyToChange.reboot) {
-        return Constants.OCPP_CONFIGURATION_RESPONSE_REBOOT_REQUIRED;
-      }
-      return Constants.OCPP_CONFIGURATION_RESPONSE_ACCEPTED;
-    }
-  }
-
-  _setChargingProfile(connectorId: number, cp: ChargingProfile): boolean {
-    if (!Utils.isEmptyArray(this.getConnector(connectorId).chargingProfiles)) {
-      this.getConnector(connectorId).chargingProfiles.forEach((chargingProfile: ChargingProfile, index: number) => {
-        if (chargingProfile.chargingProfileId === cp.chargingProfileId
-          || (chargingProfile.stackLevel === cp.stackLevel && chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)) {
-          this.getConnector(connectorId).chargingProfiles[index] = cp;
-          return true;
-        }
-      });
-    }
-    this.getConnector(connectorId).chargingProfiles.push(cp);
-    return true;
-  }
-
-  handleRequestSetChargingProfile(commandPayload: SetChargingProfileRequest): SetChargingProfileResponse {
-    if (!this.getConnector(commandPayload.connectorId)) {
-      logger.error(`${this._logPrefix()} Trying to set a charging profile to a non existing connector Id ${commandPayload.connectorId}`);
-      return Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED;
-    }
-    if (commandPayload.csChargingProfiles.chargingProfilePurpose === ChargingProfilePurposeType.CHARGE_POINT_MAX_PROFILE && commandPayload.connectorId !== 0) {
-      return Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED;
-    }
-    if (commandPayload.csChargingProfiles.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE && (commandPayload.connectorId === 0 || !this.getConnector(commandPayload.connectorId)?.transactionStarted)) {
-      return Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED;
-    }
-    this._setChargingProfile(commandPayload.connectorId, commandPayload.csChargingProfiles);
-    return Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_ACCEPTED;
-  }
-
-  handleRequestClearChargingProfile(commandPayload: ClearChargingProfileRequest): ClearChargingProfileResponse {
-    if (!this.getConnector(commandPayload.connectorId)) {
-      logger.error(`${this._logPrefix()} Trying to clear a charging profile to a non existing connector Id ${commandPayload.connectorId}`);
-      return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN;
-    }
-    if (commandPayload.connectorId && !Utils.isEmptyArray(this.getConnector(commandPayload.connectorId).chargingProfiles)) {
-      this.getConnector(commandPayload.connectorId).chargingProfiles = [];
-      return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED;
-    }
-    if (!commandPayload.connectorId) {
-      let clearedCP = false;
-      for (const connector in this.connectors) {
-        if (!Utils.isEmptyArray(this.getConnector(Utils.convertToInt(connector)).chargingProfiles)) {
-          this.getConnector(Utils.convertToInt(connector)).chargingProfiles.forEach((chargingProfile: ChargingProfile, index: number) => {
-            let clearCurrentCP = false;
-            if (chargingProfile.chargingProfileId === commandPayload.id) {
-              clearCurrentCP = true;
-            }
-            if (!commandPayload.chargingProfilePurpose && chargingProfile.stackLevel === commandPayload.stackLevel) {
-              clearCurrentCP = true;
-            }
-            if (!chargingProfile.stackLevel && chargingProfile.chargingProfilePurpose === commandPayload.chargingProfilePurpose) {
-              clearCurrentCP = true;
-            }
-            if (chargingProfile.stackLevel === commandPayload.stackLevel && chargingProfile.chargingProfilePurpose === commandPayload.chargingProfilePurpose) {
-              clearCurrentCP = true;
-            }
-            if (clearCurrentCP) {
-              this.getConnector(commandPayload.connectorId).chargingProfiles[index] = {} as ChargingProfile;
-              clearedCP = true;
-            }
-          });
-        }
-      }
-      if (clearedCP) {
-        return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED;
-      }
-    }
-    return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN;
-  }
-
-  handleRequestChangeAvailability(commandPayload: ChangeAvailabilityRequest): ChangeAvailabilityResponse {
-    const connectorId: number = commandPayload.connectorId;
-    if (!this.getConnector(connectorId)) {
-      logger.error(`${this._logPrefix()} Trying to change the availability of a non existing connector Id ${connectorId.toString()}`);
-      return Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED;
-    }
-    const chargePointStatus: ChargePointStatus = commandPayload.type === AvailabilityType.OPERATIVE ? ChargePointStatus.AVAILABLE : ChargePointStatus.UNAVAILABLE;
-    if (connectorId === 0) {
-      let response: ChangeAvailabilityResponse = Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
-      for (const connector in this.connectors) {
-        if (this.getConnector(Utils.convertToInt(connector)).transactionStarted) {
-          response = Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
-        }
-        this.getConnector(Utils.convertToInt(connector)).availability = commandPayload.type;
-        response === Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED && this.sendStatusNotification(Utils.convertToInt(connector), chargePointStatus);
-      }
-      return response;
-    } else if (connectorId > 0 && (this.getConnector(0).availability === AvailabilityType.OPERATIVE || (this.getConnector(0).availability === AvailabilityType.INOPERATIVE && commandPayload.type === AvailabilityType.INOPERATIVE))) {
-      if (this.getConnector(connectorId)?.transactionStarted) {
-        this.getConnector(connectorId).availability = commandPayload.type;
-        return Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
-      }
-      this.getConnector(connectorId).availability = commandPayload.type;
-      void this.sendStatusNotification(connectorId, chargePointStatus);
-      return Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
-    }
-    return Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED;
-  }
-
-  async handleRequestRemoteStartTransaction(commandPayload: RemoteStartTransactionRequest): Promise<DefaultResponse> {
-    const transactionConnectorID: number = commandPayload.connectorId ? commandPayload.connectorId : 1;
-    if (this._isChargingStationAvailable() && this._isConnectorAvailable(transactionConnectorID)) {
-      if (this._getAuthorizeRemoteTxRequests() && this._getLocalAuthListEnabled() && this.hasAuthorizedTags()) {
-        // Check if authorized
-        if (this.authorizedTags.find((value) => value === commandPayload.idTag)) {
-          await this.sendStatusNotification(transactionConnectorID, ChargePointStatus.PREPARING);
-          if (commandPayload.chargingProfile && commandPayload.chargingProfile.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE) {
-            this._setChargingProfile(transactionConnectorID, commandPayload.chargingProfile);
-          } else if (commandPayload.chargingProfile && commandPayload.chargingProfile.chargingProfilePurpose !== ChargingProfilePurposeType.TX_PROFILE) {
-            return Constants.OCPP_RESPONSE_REJECTED;
-          }
-          // Authorization successful start transaction
-          await this.sendStartTransaction(transactionConnectorID, commandPayload.idTag);
-          logger.debug(this._logPrefix() + ' Transaction remotely STARTED on ' + this.stationInfo.chargingStationId + '#' + transactionConnectorID.toString() + ' for idTag ' + commandPayload.idTag);
-          return Constants.OCPP_RESPONSE_ACCEPTED;
-        }
-        logger.error(this._logPrefix() + ' Remote starting transaction REJECTED on connector Id ' + transactionConnectorID.toString() + ', idTag ' + commandPayload.idTag);
-        return Constants.OCPP_RESPONSE_REJECTED;
-      }
-      await this.sendStatusNotification(transactionConnectorID, ChargePointStatus.PREPARING);
-      if (commandPayload.chargingProfile && commandPayload.chargingProfile.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE) {
-        this._setChargingProfile(transactionConnectorID, commandPayload.chargingProfile);
-      } else if (commandPayload.chargingProfile && commandPayload.chargingProfile.chargingProfilePurpose !== ChargingProfilePurposeType.TX_PROFILE) {
-        return Constants.OCPP_RESPONSE_REJECTED;
-      }
-      // No local authorization check required => start transaction
-      await this.sendStartTransaction(transactionConnectorID, commandPayload.idTag);
-      logger.debug(this._logPrefix() + ' Transaction remotely STARTED on ' + this.stationInfo.chargingStationId + '#' + transactionConnectorID.toString() + ' for idTag ' + commandPayload.idTag);
-      return Constants.OCPP_RESPONSE_ACCEPTED;
-    }
-    logger.error(this._logPrefix() + ' Remote starting transaction REJECTED on unavailable connector Id ' + transactionConnectorID.toString() + ', idTag ' + commandPayload.idTag);
-    return Constants.OCPP_RESPONSE_REJECTED;
-  }
-
-  async handleRequestRemoteStopTransaction(commandPayload: RemoteStopTransactionRequest): Promise<DefaultResponse> {
-    const transactionId = commandPayload.transactionId;
-    for (const connector in this.connectors) {
-      if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector))?.transactionId === transactionId) {
-        await this.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.FINISHING);
-        await this.sendStopTransaction(transactionId);
-        return Constants.OCPP_RESPONSE_ACCEPTED;
-      }
-    }
-    logger.info(this._logPrefix() + ' Trying to remote stop a non existing transaction ' + transactionId.toString());
-    return Constants.OCPP_RESPONSE_REJECTED;
-  }
-
-  // eslint-disable-next-line consistent-this
-  private async sendMeterValues(connectorId: number, interval: number, self: ChargingStation, debug = false): Promise<void> {
-    try {
-      const meterValue: MeterValue = {
-        timestamp: new Date().toISOString(),
-        sampledValue: [],
-      };
-      const meterValuesTemplate: SampledValue[] = self.getConnector(connectorId).MeterValues;
-      for (let index = 0; index < meterValuesTemplate.length; index++) {
-        const connector = self.getConnector(connectorId);
-        // SoC measurand
-        if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === MeterValueMeasurand.STATE_OF_CHARGE && self._getConfigurationKey(StandardParametersKey.MeterValuesSampledData).value.includes(MeterValueMeasurand.STATE_OF_CHARGE)) {
-          meterValue.sampledValue.push({
-            ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.PERCENT },
-            ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
-            measurand: meterValuesTemplate[index].measurand,
-            ...!Utils.isUndefined(meterValuesTemplate[index].location) ? { location: meterValuesTemplate[index].location } : { location: MeterValueLocation.EV },
-            ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: Utils.getRandomInt(100).toString() },
-          });
-          const sampledValuesIndex = meterValue.sampledValue.length - 1;
-          if (Utils.convertToInt(meterValue.sampledValue[sampledValuesIndex].value) > 100 || debug) {
-            logger.error(`${self._logPrefix()} MeterValues measurand ${meterValue.sampledValue[sampledValuesIndex].measurand ? meterValue.sampledValue[sampledValuesIndex].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${meterValue.sampledValue[sampledValuesIndex].value}/100`);
-          }
-        // Voltage measurand
-        } else if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === MeterValueMeasurand.VOLTAGE && self._getConfigurationKey(StandardParametersKey.MeterValuesSampledData).value.includes(MeterValueMeasurand.VOLTAGE)) {
-          const voltageMeasurandValue = Utils.getRandomFloatRounded(self._getVoltageOut() + self._getVoltageOut() * 0.1, self._getVoltageOut() - self._getVoltageOut() * 0.1);
-          meterValue.sampledValue.push({
-            ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.VOLT },
-            ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
-            measurand: meterValuesTemplate[index].measurand,
-            ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
-            ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: voltageMeasurandValue.toString() },
-          });
-          for (let phase = 1; self._getNumberOfPhases() === 3 && phase <= self._getNumberOfPhases(); phase++) {
-            let phaseValue: string;
-            if (self._getVoltageOut() >= 0 && self._getVoltageOut() <= 250) {
-              phaseValue = `L${phase}-N`;
-            } else if (self._getVoltageOut() > 250) {
-              phaseValue = `L${phase}-L${(phase + 1) % self._getNumberOfPhases() !== 0 ? (phase + 1) % self._getNumberOfPhases() : self._getNumberOfPhases()}`;
-            }
-            meterValue.sampledValue.push({
-              ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.VOLT },
-              ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
-              measurand: meterValuesTemplate[index].measurand,
-              ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
-              ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: voltageMeasurandValue.toString() },
-              phase: phaseValue as MeterValuePhase,
-            });
-          }
-        // Power.Active.Import measurand
-        } else if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === MeterValueMeasurand.POWER_ACTIVE_IMPORT && self._getConfigurationKey(StandardParametersKey.MeterValuesSampledData).value.includes(MeterValueMeasurand.POWER_ACTIVE_IMPORT)) {
-          // FIXME: factor out powerDivider checks
-          if (Utils.isUndefined(self.stationInfo.powerDivider)) {
-            const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider is undefined`;
-            logger.error(errMsg);
-            throw Error(errMsg);
-          } else if (self.stationInfo.powerDivider && self.stationInfo.powerDivider <= 0) {
-            const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider have zero or below value ${self.stationInfo.powerDivider}`;
-            logger.error(errMsg);
-            throw Error(errMsg);
-          }
-          const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: Unknown ${self._getPowerOutType()} powerOutType in template file ${self.stationTemplateFile}, cannot calculate ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER} measurand value`;
-          const powerMeasurandValues = {} as MeasurandValues;
-          const maxPower = Math.round(self.stationInfo.maxPower / self.stationInfo.powerDivider);
-          const maxPowerPerPhase = Math.round((self.stationInfo.maxPower / self.stationInfo.powerDivider) / self._getNumberOfPhases());
-          switch (self._getPowerOutType()) {
-            case PowerOutType.AC:
-              if (Utils.isUndefined(meterValuesTemplate[index].value)) {
-                powerMeasurandValues.L1 = Utils.getRandomFloatRounded(maxPowerPerPhase);
-                powerMeasurandValues.L2 = 0;
-                powerMeasurandValues.L3 = 0;
-                if (self._getNumberOfPhases() === 3) {
-                  powerMeasurandValues.L2 = Utils.getRandomFloatRounded(maxPowerPerPhase);
-                  powerMeasurandValues.L3 = Utils.getRandomFloatRounded(maxPowerPerPhase);
-                }
-                powerMeasurandValues.allPhases = Utils.roundTo(powerMeasurandValues.L1 + powerMeasurandValues.L2 + powerMeasurandValues.L3, 2);
-              }
-              break;
-            case PowerOutType.DC:
-              if (Utils.isUndefined(meterValuesTemplate[index].value)) {
-                powerMeasurandValues.allPhases = Utils.getRandomFloatRounded(maxPower);
-              }
-              break;
-            default:
-              logger.error(errMsg);
-              throw Error(errMsg);
-          }
-          meterValue.sampledValue.push({
-            ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.WATT },
-            ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
-            measurand: meterValuesTemplate[index].measurand,
-            ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
-            ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: powerMeasurandValues.allPhases.toString() },
-          });
-          const sampledValuesIndex = meterValue.sampledValue.length - 1;
-          if (Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) > maxPower || debug) {
-            logger.error(`${self._logPrefix()} MeterValues measurand ${meterValue.sampledValue[sampledValuesIndex].measurand ? meterValue.sampledValue[sampledValuesIndex].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${meterValue.sampledValue[sampledValuesIndex].value}/${maxPower}`);
-          }
-          for (let phase = 1; self._getNumberOfPhases() === 3 && phase <= self._getNumberOfPhases(); phase++) {
-            const phaseValue = `L${phase}-N`;
-            meterValue.sampledValue.push({
-              ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.WATT },
-              ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
-              ...!Utils.isUndefined(meterValuesTemplate[index].measurand) && { measurand: meterValuesTemplate[index].measurand },
-              ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
-              ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: powerMeasurandValues[`L${phase}`] as string },
-              phase: phaseValue as MeterValuePhase,
-            });
-          }
-        // Current.Import measurand
-        } else if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === MeterValueMeasurand.CURRENT_IMPORT && self._getConfigurationKey(StandardParametersKey.MeterValuesSampledData).value.includes(MeterValueMeasurand.CURRENT_IMPORT)) {
-          // FIXME: factor out powerDivider checks
-          if (Utils.isUndefined(self.stationInfo.powerDivider)) {
-            const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider is undefined`;
-            logger.error(errMsg);
-            throw Error(errMsg);
-          } else if (self.stationInfo.powerDivider && self.stationInfo.powerDivider <= 0) {
-            const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider have zero or below value ${self.stationInfo.powerDivider}`;
-            logger.error(errMsg);
-            throw Error(errMsg);
-          }
-          const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: Unknown ${self._getPowerOutType()} powerOutType in template file ${self.stationTemplateFile}, cannot calculate ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER} measurand value`;
-          const currentMeasurandValues: MeasurandValues = {} as MeasurandValues;
-          let maxAmperage: number;
-          switch (self._getPowerOutType()) {
-            case PowerOutType.AC:
-              maxAmperage = ElectricUtils.ampPerPhaseFromPower(self._getNumberOfPhases(), self.stationInfo.maxPower / self.stationInfo.powerDivider, self._getVoltageOut());
-              if (Utils.isUndefined(meterValuesTemplate[index].value)) {
-                currentMeasurandValues.L1 = Utils.getRandomFloatRounded(maxAmperage);
-                currentMeasurandValues.L2 = 0;
-                currentMeasurandValues.L3 = 0;
-                if (self._getNumberOfPhases() === 3) {
-                  currentMeasurandValues.L2 = Utils.getRandomFloatRounded(maxAmperage);
-                  currentMeasurandValues.L3 = Utils.getRandomFloatRounded(maxAmperage);
-                }
-                currentMeasurandValues.allPhases = Utils.roundTo((currentMeasurandValues.L1 + currentMeasurandValues.L2 + currentMeasurandValues.L3) / self._getNumberOfPhases(), 2);
-              }
-              break;
-            case PowerOutType.DC:
-              maxAmperage = ElectricUtils.ampTotalFromPower(self.stationInfo.maxPower / self.stationInfo.powerDivider, self._getVoltageOut());
-              if (Utils.isUndefined(meterValuesTemplate[index].value)) {
-                currentMeasurandValues.allPhases = Utils.getRandomFloatRounded(maxAmperage);
-              }
-              break;
-            default:
-              logger.error(errMsg);
-              throw Error(errMsg);
-          }
-          meterValue.sampledValue.push({
-            ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.AMP },
-            ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
-            measurand: meterValuesTemplate[index].measurand,
-            ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
-            ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: currentMeasurandValues.allPhases.toString() },
-          });
-          const sampledValuesIndex = meterValue.sampledValue.length - 1;
-          if (Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) > maxAmperage || debug) {
-            logger.error(`${self._logPrefix()} MeterValues measurand ${meterValue.sampledValue[sampledValuesIndex].measurand ? meterValue.sampledValue[sampledValuesIndex].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${meterValue.sampledValue[sampledValuesIndex].value}/${maxAmperage}`);
-          }
-          for (let phase = 1; self._getNumberOfPhases() === 3 && phase <= self._getNumberOfPhases(); phase++) {
-            const phaseValue = `L${phase}`;
-            meterValue.sampledValue.push({
-              ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.AMP },
-              ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
-              ...!Utils.isUndefined(meterValuesTemplate[index].measurand) && { measurand: meterValuesTemplate[index].measurand },
-              ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
-              ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: currentMeasurandValues[phaseValue] as string },
-              phase: phaseValue as MeterValuePhase,
-            });
-          }
-        // Energy.Active.Import.Register measurand (default)
-        } else if (!meterValuesTemplate[index].measurand || meterValuesTemplate[index].measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
-          // FIXME: factor out powerDivider checks
-          if (Utils.isUndefined(self.stationInfo.powerDivider)) {
-            const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider is undefined`;
-            logger.error(errMsg);
-            throw Error(errMsg);
-          } else if (self.stationInfo.powerDivider && self.stationInfo.powerDivider <= 0) {
-            const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider have zero or below value ${self.stationInfo.powerDivider}`;
-            logger.error(errMsg);
-            throw Error(errMsg);
-          }
-          if (Utils.isUndefined(meterValuesTemplate[index].value)) {
-            const measurandValue = Utils.getRandomInt(self.stationInfo.maxPower / (self.stationInfo.powerDivider * 3600000) * interval);
-            // Persist previous value in connector
-            if (connector && !Utils.isNullOrUndefined(connector.lastEnergyActiveImportRegisterValue) && connector.lastEnergyActiveImportRegisterValue >= 0) {
-              connector.lastEnergyActiveImportRegisterValue += measurandValue;
-            } else {
-              connector.lastEnergyActiveImportRegisterValue = 0;
-            }
-          }
-          meterValue.sampledValue.push({
-            ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.WATT_HOUR },
-            ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
-            ...!Utils.isUndefined(meterValuesTemplate[index].measurand) && { measurand: meterValuesTemplate[index].measurand },
-            ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
-            ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } :
-              { value: connector.lastEnergyActiveImportRegisterValue.toString() },
-          });
-          const sampledValuesIndex = meterValue.sampledValue.length - 1;
-          const maxConsumption = Math.round(self.stationInfo.maxPower * 3600 / (self.stationInfo.powerDivider * interval));
-          if (Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) > maxConsumption || debug) {
-            logger.error(`${self._logPrefix()} MeterValues measurand ${meterValue.sampledValue[sampledValuesIndex].measurand ? meterValue.sampledValue[sampledValuesIndex].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${meterValue.sampledValue[sampledValuesIndex].value}/${maxConsumption}`);
-          }
-        // Unsupported measurand
-        } else {
-          logger.info(`${self._logPrefix()} Unsupported MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER} on connectorId ${connectorId}`);
-        }
-      }
-      const payload: MeterValuesRequest = {
-        connectorId,
-        transactionId: self.getConnector(connectorId).transactionId,
-        meterValue: meterValue,
-      };
-      await self.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, RequestCommand.METERVALUES);
-    } catch (error) {
-      this.handleRequestError(RequestCommand.METERVALUES, error);
-    }
-  }
-
-  private handleRequestError(commandName: RequestCommand, error: Error) {
-    logger.error(this._logPrefix() + ' Send ' + commandName + ' error: %j', error);
-    throw error;
-  }
 }
 
index 1b529190cb10c99f98c0b2ce5b9b17a48fc29a1c..65e2fb8832883662aadb145798da9783f37a1ae2 100644 (file)
@@ -6,6 +6,12 @@ import Constants from '../utils/Constants';
 import { ThreadWorker } from 'poolifier';
 import Utils from '../utils/Utils';
 
+// Conditionally export ThreadWorker instance for pool usage
+export let threadWorker;
+if (Utils.workerPoolInUse()) {
+  threadWorker = new ThreadWorker(startChargingStation, { maxInactiveTime: Constants.WORKER_POOL_MAX_INACTIVE_TIME, async: false });
+}
+
 if (!isMainThread) {
   // Add listener to start charging station from main thread
   addListener();
@@ -26,5 +32,3 @@ function startChargingStation(data: StationWorkerData) {
   const station = new ChargingStation(data.index , data.templateFile);
   station.start();
 }
-
-export default new ThreadWorker(startChargingStation, { maxInactiveTime: Constants.WORKER_POOL_MAX_INACTIVE_TIME, async: false });
diff --git a/src/charging-station/ocpp/1.6/OCCP16IncomingRequestService.ts b/src/charging-station/ocpp/1.6/OCCP16IncomingRequestService.ts
new file mode 100644 (file)
index 0000000..38bd3c0
--- /dev/null
@@ -0,0 +1,305 @@
+import { ChangeAvailabilityRequest, ChangeConfigurationRequest, ClearChargingProfileRequest, GetConfigurationRequest, OCPP16AvailabilityType, OCPP16IncomingRequestCommand, RemoteStartTransactionRequest, RemoteStopTransactionRequest, ResetRequest, SetChargingProfileRequest, UnlockConnectorRequest } from '../../../types/ocpp/1.6/Requests';
+import { ChangeAvailabilityResponse, ChangeConfigurationResponse, ClearChargingProfileResponse, DefaultResponse, GetConfigurationResponse, SetChargingProfileResponse, UnlockConnectorResponse } from '../../../types/ocpp/1.6/RequestResponses';
+import { ChargingProfilePurposeType, OCPP16ChargingProfile } from '../../../types/ocpp/1.6/ChargingProfile';
+import { OCPP16AuthorizationStatus, OCPP16StopTransactionReason } from '../../../types/ocpp/1.6/Transaction';
+
+import Constants from '../../../utils/Constants';
+import { ErrorType } from '../../../types/ocpp/ErrorType';
+import { MessageType } from '../../../types/ocpp/MessageType';
+import { OCPP16ChargePointStatus } from '../../../types/ocpp/1.6/ChargePointStatus';
+import { OCPP16StandardParametersKey } from '../../../types/ocpp/1.6/Configuration';
+import { OCPPConfigurationKey } from '../../../types/ocpp/Configuration';
+import OCPPError from '../../OcppError';
+import OCPPIncomingRequestService from '../OCPPIncomingRequestService';
+import Utils from '../../../utils/Utils';
+import logger from '../../../utils/Logger';
+
+export default class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
+  public async handleRequest(messageId: string, commandName: OCPP16IncomingRequestCommand, commandPayload: Record<string, unknown>): Promise<void> {
+    let response;
+    // Call
+    if (typeof this['handleRequest' + commandName] === 'function') {
+      try {
+        // Call the method to build the response
+        response = await this['handleRequest' + commandName](commandPayload);
+      } catch (error) {
+        // Log
+        logger.error(this.chargingStation.logPrefix() + ' Handle request error: %j', error);
+        // Send back an error response to inform backend
+        await this.chargingStation.ocppRequestService.sendError(messageId, error, commandName);
+        throw error;
+      }
+    } else {
+      // Throw exception
+      await this.chargingStation.ocppRequestService.sendError(messageId, new OCPPError(ErrorType.NOT_IMPLEMENTED, `${commandName} is not implemented`, {}), commandName);
+      throw new Error(`${commandName} is not implemented to handle payload ${JSON.stringify(commandPayload)}`);
+    }
+    // Send the built response
+    await this.chargingStation.ocppRequestService.sendMessage(messageId, response, MessageType.CALL_RESULT_MESSAGE, commandName);
+  }
+
+  // Simulate charging station restart
+  private handleRequestReset(commandPayload: ResetRequest): DefaultResponse {
+    // eslint-disable-next-line @typescript-eslint/no-misused-promises
+    setImmediate(async (): Promise<void> => {
+      await this.chargingStation.stop(commandPayload.type + 'Reset' as OCPP16StopTransactionReason);
+      await Utils.sleep(this.chargingStation.stationInfo.resetTime);
+      this.chargingStation.start();
+    });
+    logger.info(`${this.chargingStation.logPrefix()} ${commandPayload.type} reset command received, simulating it. The station will be back online in ${Utils.milliSecondsToHHMMSS(this.chargingStation.stationInfo.resetTime)}`);
+    return Constants.OCPP_RESPONSE_ACCEPTED;
+  }
+
+  private handleRequestClearCache(): DefaultResponse {
+    return Constants.OCPP_RESPONSE_ACCEPTED;
+  }
+
+  private async handleRequestUnlockConnector(commandPayload: UnlockConnectorRequest): Promise<UnlockConnectorResponse> {
+    const connectorId = commandPayload.connectorId;
+    if (connectorId === 0) {
+      logger.error(this.chargingStation.logPrefix() + ' Trying to unlock connector ' + connectorId.toString());
+      return Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED;
+    }
+    if (this.chargingStation.getConnector(connectorId)?.transactionStarted) {
+      const transactionId = this.chargingStation.getConnector(connectorId).transactionId;
+      const stopResponse = await this.chargingStation.ocppRequestService.sendStopTransaction(transactionId, this.chargingStation.getTransactionMeterStop(transactionId), this.chargingStation.getTransactionIdTag(transactionId), OCPP16StopTransactionReason.UNLOCK_COMMAND);
+      if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
+        return Constants.OCPP_RESPONSE_UNLOCKED;
+      }
+      return Constants.OCPP_RESPONSE_UNLOCK_FAILED;
+    }
+    await this.chargingStation.ocppRequestService.sendStatusNotification(connectorId, OCPP16ChargePointStatus.AVAILABLE);
+    this.chargingStation.getConnector(connectorId).status = OCPP16ChargePointStatus.AVAILABLE;
+    return Constants.OCPP_RESPONSE_UNLOCKED;
+  }
+
+  private handleRequestGetConfiguration(commandPayload: GetConfigurationRequest): GetConfigurationResponse {
+    const configurationKey: OCPPConfigurationKey[] = [];
+    const unknownKey: string[] = [];
+    if (Utils.isEmptyArray(commandPayload.key)) {
+      for (const configuration of this.chargingStation.configuration.configurationKey) {
+        if (Utils.isUndefined(configuration.visible)) {
+          configuration.visible = true;
+        }
+        if (!configuration.visible) {
+          continue;
+        }
+        configurationKey.push({
+          key: configuration.key,
+          readonly: configuration.readonly,
+          value: configuration.value,
+        });
+      }
+    } else {
+      for (const key of commandPayload.key) {
+        const keyFound = this.chargingStation.getConfigurationKey(key);
+        if (keyFound) {
+          if (Utils.isUndefined(keyFound.visible)) {
+            keyFound.visible = true;
+          }
+          if (!keyFound.visible) {
+            continue;
+          }
+          configurationKey.push({
+            key: keyFound.key,
+            readonly: keyFound.readonly,
+            value: keyFound.value,
+          });
+        } else {
+          unknownKey.push(key);
+        }
+      }
+    }
+    return {
+      configurationKey,
+      unknownKey,
+    };
+  }
+
+  private handleRequestChangeConfiguration(commandPayload: ChangeConfigurationRequest): ChangeConfigurationResponse {
+    // JSON request fields type sanity check
+    if (!Utils.isString(commandPayload.key)) {
+      logger.error(`${this.chargingStation.logPrefix()} ChangeConfiguration request key field is not a string:`, commandPayload);
+    }
+    if (!Utils.isString(commandPayload.value)) {
+      logger.error(`${this.chargingStation.logPrefix()} ChangeConfiguration request value field is not a string:`, commandPayload);
+    }
+    const keyToChange = this.chargingStation.getConfigurationKey(commandPayload.key, true);
+    if (!keyToChange) {
+      return Constants.OCPP_CONFIGURATION_RESPONSE_NOT_SUPPORTED;
+    } else if (keyToChange && keyToChange.readonly) {
+      return Constants.OCPP_CONFIGURATION_RESPONSE_REJECTED;
+    } else if (keyToChange && !keyToChange.readonly) {
+      const keyIndex = this.chargingStation.configuration.configurationKey.indexOf(keyToChange);
+      let valueChanged = false;
+      if (this.chargingStation.configuration.configurationKey[keyIndex].value !== commandPayload.value) {
+        this.chargingStation.configuration.configurationKey[keyIndex].value = commandPayload.value;
+        valueChanged = true;
+      }
+      let triggerHeartbeatRestart = false;
+      if (keyToChange.key === OCPP16StandardParametersKey.HeartBeatInterval && valueChanged) {
+        this.chargingStation.setConfigurationKeyValue(OCPP16StandardParametersKey.HeartbeatInterval, commandPayload.value);
+        triggerHeartbeatRestart = true;
+      }
+      if (keyToChange.key === OCPP16StandardParametersKey.HeartbeatInterval && valueChanged) {
+        this.chargingStation.setConfigurationKeyValue(OCPP16StandardParametersKey.HeartBeatInterval, commandPayload.value);
+        triggerHeartbeatRestart = true;
+      }
+      if (triggerHeartbeatRestart) {
+        this.chargingStation.restartHeartbeat();
+      }
+      if (keyToChange.key === OCPP16StandardParametersKey.WebSocketPingInterval && valueChanged) {
+        this.chargingStation.restartWebSocketPing();
+      }
+      if (keyToChange.reboot) {
+        return Constants.OCPP_CONFIGURATION_RESPONSE_REBOOT_REQUIRED;
+      }
+      return Constants.OCPP_CONFIGURATION_RESPONSE_ACCEPTED;
+    }
+  }
+
+  private handleRequestSetChargingProfile(commandPayload: SetChargingProfileRequest): SetChargingProfileResponse {
+    if (!this.chargingStation.getConnector(commandPayload.connectorId)) {
+      logger.error(`${this.chargingStation.logPrefix()} Trying to set charging profile(s) to a non existing connector Id ${commandPayload.connectorId}`);
+      return Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED;
+    }
+    if (commandPayload.csChargingProfiles.chargingProfilePurpose === ChargingProfilePurposeType.CHARGE_POINT_MAX_PROFILE && commandPayload.connectorId !== 0) {
+      return Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED;
+    }
+    if (commandPayload.csChargingProfiles.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE && (commandPayload.connectorId === 0 || !this.chargingStation.getConnector(commandPayload.connectorId)?.transactionStarted)) {
+      return Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED;
+    }
+    this.chargingStation.setChargingProfile(commandPayload.connectorId, commandPayload.csChargingProfiles);
+    logger.debug(`${this.chargingStation.logPrefix()} Charging profile(s) set, dump their stack: %j`, this.chargingStation.getConnector(commandPayload.connectorId).chargingProfiles);
+    return Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_ACCEPTED;
+  }
+
+  private handleRequestClearChargingProfile(commandPayload: ClearChargingProfileRequest): ClearChargingProfileResponse {
+    if (!this.chargingStation.getConnector(commandPayload.connectorId)) {
+      logger.error(`${this.chargingStation.logPrefix()} Trying to clear a charging profile(s) to a non existing connector Id ${commandPayload.connectorId}`);
+      return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN;
+    }
+    if (commandPayload.connectorId && !Utils.isEmptyArray(this.chargingStation.getConnector(commandPayload.connectorId).chargingProfiles)) {
+      this.chargingStation.getConnector(commandPayload.connectorId).chargingProfiles = [];
+      logger.debug(`${this.chargingStation.logPrefix()} Charging profile(s) cleared, dump their stack: %j`, this.chargingStation.getConnector(commandPayload.connectorId).chargingProfiles);
+      return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED;
+    }
+    if (!commandPayload.connectorId) {
+      let clearedCP = false;
+      for (const connector in this.chargingStation.connectors) {
+        if (!Utils.isEmptyArray(this.chargingStation.getConnector(Utils.convertToInt(connector)).chargingProfiles)) {
+          this.chargingStation.getConnector(Utils.convertToInt(connector)).chargingProfiles.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
+            let clearCurrentCP = false;
+            if (chargingProfile.chargingProfileId === commandPayload.id) {
+              clearCurrentCP = true;
+            }
+            if (!commandPayload.chargingProfilePurpose && chargingProfile.stackLevel === commandPayload.stackLevel) {
+              clearCurrentCP = true;
+            }
+            if (!chargingProfile.stackLevel && chargingProfile.chargingProfilePurpose === commandPayload.chargingProfilePurpose) {
+              clearCurrentCP = true;
+            }
+            if (chargingProfile.stackLevel === commandPayload.stackLevel && chargingProfile.chargingProfilePurpose === commandPayload.chargingProfilePurpose) {
+              clearCurrentCP = true;
+            }
+            if (clearCurrentCP) {
+              this.chargingStation.getConnector(commandPayload.connectorId).chargingProfiles[index] = {} as OCPP16ChargingProfile;
+              logger.debug(`${this.chargingStation.logPrefix()} Charging profile(s) cleared, dump their stack: %j`, this.chargingStation.getConnector(commandPayload.connectorId).chargingProfiles);
+              clearedCP = true;
+            }
+          });
+        }
+      }
+      if (clearedCP) {
+        return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED;
+      }
+    }
+    return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN;
+  }
+
+  private async handleRequestChangeAvailability(commandPayload: ChangeAvailabilityRequest): Promise<ChangeAvailabilityResponse> {
+    const connectorId: number = commandPayload.connectorId;
+    if (!this.chargingStation.getConnector(connectorId)) {
+      logger.error(`${this.chargingStation.logPrefix()} Trying to change the availability of a non existing connector Id ${connectorId.toString()}`);
+      return Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED;
+    }
+    const chargePointStatus: OCPP16ChargePointStatus = commandPayload.type === OCPP16AvailabilityType.OPERATIVE ? OCPP16ChargePointStatus.AVAILABLE : OCPP16ChargePointStatus.UNAVAILABLE;
+    if (connectorId === 0) {
+      let response: ChangeAvailabilityResponse = Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
+      for (const connector in this.chargingStation.connectors) {
+        if (this.chargingStation.getConnector(Utils.convertToInt(connector)).transactionStarted) {
+          response = Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
+        }
+        this.chargingStation.getConnector(Utils.convertToInt(connector)).availability = commandPayload.type;
+        if (response === Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED) {
+          await this.chargingStation.ocppRequestService.sendStatusNotification(Utils.convertToInt(connector), chargePointStatus);
+          this.chargingStation.getConnector(Utils.convertToInt(connector)).status = chargePointStatus;
+        }
+      }
+      return response;
+    } else if (connectorId > 0 && (this.chargingStation.getConnector(0).availability === OCPP16AvailabilityType.OPERATIVE || (this.chargingStation.getConnector(0).availability === OCPP16AvailabilityType.INOPERATIVE && commandPayload.type === OCPP16AvailabilityType.INOPERATIVE))) {
+      if (this.chargingStation.getConnector(connectorId)?.transactionStarted) {
+        this.chargingStation.getConnector(connectorId).availability = commandPayload.type;
+        return Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
+      }
+      this.chargingStation.getConnector(connectorId).availability = commandPayload.type;
+      await this.chargingStation.ocppRequestService.sendStatusNotification(connectorId, chargePointStatus);
+      this.chargingStation.getConnector(connectorId).status = chargePointStatus;
+      return Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
+    }
+    return Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED;
+  }
+
+  private async handleRequestRemoteStartTransaction(commandPayload: RemoteStartTransactionRequest): Promise<DefaultResponse> {
+    const transactionConnectorID: number = commandPayload.connectorId ? commandPayload.connectorId : 1;
+    if (this.chargingStation.isChargingStationAvailable() && this.chargingStation.isConnectorAvailable(transactionConnectorID)) {
+      if (this.chargingStation.getAuthorizeRemoteTxRequests() && this.chargingStation.getLocalAuthListEnabled() && this.chargingStation.hasAuthorizedTags()) {
+        // Check if authorized
+        if (this.chargingStation.authorizedTags.find((value) => value === commandPayload.idTag)) {
+          await this.chargingStation.ocppRequestService.sendStatusNotification(transactionConnectorID, OCPP16ChargePointStatus.PREPARING);
+          this.chargingStation.getConnector(transactionConnectorID).status = OCPP16ChargePointStatus.PREPARING;
+          if (commandPayload.chargingProfile && commandPayload.chargingProfile.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE) {
+            this.chargingStation.setChargingProfile(transactionConnectorID, commandPayload.chargingProfile);
+            logger.debug(`${this.chargingStation.logPrefix()} Charging profile(s) set at start transaction, dump their stack: %j`, this.chargingStation.getConnector(transactionConnectorID).chargingProfiles);
+          } else if (commandPayload.chargingProfile && commandPayload.chargingProfile.chargingProfilePurpose !== ChargingProfilePurposeType.TX_PROFILE) {
+            return Constants.OCPP_RESPONSE_REJECTED;
+          }
+          // Authorization successful start transaction
+          await this.chargingStation.ocppRequestService.sendStartTransaction(transactionConnectorID, commandPayload.idTag);
+          logger.debug(this.chargingStation.logPrefix() + ' Transaction remotely STARTED on ' + this.chargingStation.stationInfo.chargingStationId + '#' + transactionConnectorID.toString() + ' for idTag ' + commandPayload.idTag);
+          return Constants.OCPP_RESPONSE_ACCEPTED;
+        }
+        logger.error(this.chargingStation.logPrefix() + ' Remote starting transaction REJECTED on connector Id ' + transactionConnectorID.toString() + ', idTag ' + commandPayload.idTag);
+        return Constants.OCPP_RESPONSE_REJECTED;
+      }
+      await this.chargingStation.ocppRequestService.sendStatusNotification(transactionConnectorID, OCPP16ChargePointStatus.PREPARING);
+      this.chargingStation.getConnector(transactionConnectorID).status = OCPP16ChargePointStatus.PREPARING;
+      if (commandPayload.chargingProfile && commandPayload.chargingProfile.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE) {
+        this.chargingStation.setChargingProfile(transactionConnectorID, commandPayload.chargingProfile);
+        logger.debug(`${this.chargingStation.logPrefix()} Charging profile(s) set at start transaction, dump their stack: %j`, this.chargingStation.getConnector(commandPayload.connectorId).chargingProfiles);
+      } else if (commandPayload.chargingProfile && commandPayload.chargingProfile.chargingProfilePurpose !== ChargingProfilePurposeType.TX_PROFILE) {
+        return Constants.OCPP_RESPONSE_REJECTED;
+      }
+      // No local authorization check required => start transaction
+      await this.chargingStation.ocppRequestService.sendStartTransaction(transactionConnectorID, commandPayload.idTag);
+      logger.debug(this.chargingStation.logPrefix() + ' Transaction remotely STARTED on ' + this.chargingStation.stationInfo.chargingStationId + '#' + transactionConnectorID.toString() + ' for idTag ' + commandPayload.idTag);
+      return Constants.OCPP_RESPONSE_ACCEPTED;
+    }
+    logger.error(this.chargingStation.logPrefix() + ' Remote starting transaction REJECTED on unavailable connector Id ' + transactionConnectorID.toString() + ', idTag ' + commandPayload.idTag);
+    return Constants.OCPP_RESPONSE_REJECTED;
+  }
+
+  private async handleRequestRemoteStopTransaction(commandPayload: RemoteStopTransactionRequest): Promise<DefaultResponse> {
+    const transactionId = commandPayload.transactionId;
+    for (const connector in this.chargingStation.connectors) {
+      if (Utils.convertToInt(connector) > 0 && this.chargingStation.getConnector(Utils.convertToInt(connector))?.transactionId === transactionId) {
+        await this.chargingStation.ocppRequestService.sendStatusNotification(Utils.convertToInt(connector), OCPP16ChargePointStatus.FINISHING);
+        this.chargingStation.getConnector(Utils.convertToInt(connector)).status = OCPP16ChargePointStatus.FINISHING;
+        await this.chargingStation.ocppRequestService.sendStopTransaction(transactionId, this.chargingStation.getTransactionMeterStop(transactionId), this.chargingStation.getTransactionIdTag(transactionId));
+        return Constants.OCPP_RESPONSE_ACCEPTED;
+      }
+    }
+    logger.info(this.chargingStation.logPrefix() + ' Trying to remote stop a non existing transaction ' + transactionId.toString());
+    return Constants.OCPP_RESPONSE_REJECTED;
+  }
+}
diff --git a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts
new file mode 100644 (file)
index 0000000..a437e0b
--- /dev/null
@@ -0,0 +1,325 @@
+import { AuthorizeRequest, OCPP16AuthorizeResponse, OCPP16StartTransactionResponse, OCPP16StopTransactionReason, OCPP16StopTransactionResponse, StartTransactionRequest, StopTransactionRequest } from '../../../types/ocpp/1.6/Transaction';
+import { HeartbeatRequest, OCPP16BootNotificationRequest, OCPP16IncomingRequestCommand, OCPP16RequestCommand, StatusNotificationRequest } from '../../../types/ocpp/1.6/Requests';
+import { MeterValue, MeterValueLocation, MeterValuePhase, MeterValueUnit, MeterValuesRequest, OCPP16MeterValueMeasurand, OCPP16SampledValue } from '../../../types/ocpp/1.6/MeterValues';
+
+import Constants from '../../../utils/Constants';
+import ElectricUtils from '../../../utils/ElectricUtils';
+import MeasurandValues from '../../../types/MeasurandValues';
+import { MessageType } from '../../../types/ocpp/MessageType';
+import { OCPP16BootNotificationResponse } from '../../../types/ocpp/1.6/RequestResponses';
+import { OCPP16ChargePointErrorCode } from '../../../types/ocpp/1.6/ChargePointErrorCode';
+import { OCPP16ChargePointStatus } from '../../../types/ocpp/1.6/ChargePointStatus';
+import { OCPP16StandardParametersKey } from '../../../types/ocpp/1.6/Configuration';
+import OCPPError from '../../OcppError';
+import OCPPRequestService from '../OCPPRequestService';
+import { PowerOutType } from '../../../types/ChargingStationTemplate';
+import Utils from '../../../utils/Utils';
+import logger from '../../../utils/Logger';
+
+export default class OCPP16RequestService extends OCPPRequestService {
+  public async sendHeartbeat(): Promise<void> {
+    try {
+      const payload: HeartbeatRequest = {};
+      await this.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, OCPP16RequestCommand.HEARTBEAT);
+    } catch (error) {
+      this.handleRequestError(OCPP16RequestCommand.HEARTBEAT, error);
+    }
+  }
+
+  public async sendBootNotification(chargePointModel: string, chargePointVendor: string, chargeBoxSerialNumber?: string, firmwareVersion?: string, chargePointSerialNumber?: string, iccid?: string, imsi?: string, meterSerialNumber?: string, meterType?: string): Promise<OCPP16BootNotificationResponse> {
+    try {
+      const payload: OCPP16BootNotificationRequest = {
+        chargePointModel,
+        chargePointVendor,
+        ...!Utils.isUndefined(chargeBoxSerialNumber) && { chargeBoxSerialNumber },
+        ...!Utils.isUndefined(chargePointSerialNumber) && { chargePointSerialNumber },
+        ...!Utils.isUndefined(firmwareVersion) && { firmwareVersion },
+        ...!Utils.isUndefined(iccid) && { iccid },
+        ...!Utils.isUndefined(imsi) && { imsi },
+        ...!Utils.isUndefined(meterSerialNumber) && { meterSerialNumber },
+        ...!Utils.isUndefined(meterType) && { meterType }
+      };
+      return await this.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, OCPP16RequestCommand.BOOT_NOTIFICATION) as OCPP16BootNotificationResponse;
+    } catch (error) {
+      this.handleRequestError(OCPP16RequestCommand.BOOT_NOTIFICATION, error);
+    }
+  }
+
+  public async sendStatusNotification(connectorId: number, status: OCPP16ChargePointStatus, errorCode: OCPP16ChargePointErrorCode = OCPP16ChargePointErrorCode.NO_ERROR): Promise<void> {
+    try {
+      const payload: StatusNotificationRequest = {
+        connectorId,
+        errorCode,
+        status,
+      };
+      await this.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, OCPP16RequestCommand.STATUS_NOTIFICATION);
+    } catch (error) {
+      this.handleRequestError(OCPP16RequestCommand.STATUS_NOTIFICATION, error);
+    }
+  }
+
+  public async sendAuthorize(idTag?: string): Promise<OCPP16AuthorizeResponse> {
+    try {
+      const payload: AuthorizeRequest = {
+        ...!Utils.isUndefined(idTag) ? { idTag } : { idTag: Constants.TRANSACTION_DEFAULT_TAGID },
+      };
+      return await this.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, OCPP16RequestCommand.AUTHORIZE) as OCPP16AuthorizeResponse;
+    } catch (error) {
+      this.handleRequestError(OCPP16RequestCommand.AUTHORIZE, error);
+    }
+  }
+
+  public async sendStartTransaction(connectorId: number, idTag?: string): Promise<OCPP16StartTransactionResponse> {
+    try {
+      const payload: StartTransactionRequest = {
+        connectorId,
+        ...!Utils.isUndefined(idTag) ? { idTag } : { idTag: Constants.TRANSACTION_DEFAULT_TAGID },
+        meterStart: 0,
+        timestamp: new Date().toISOString(),
+      };
+      return await this.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, OCPP16RequestCommand.START_TRANSACTION) as OCPP16StartTransactionResponse;
+    } catch (error) {
+      this.handleRequestError(OCPP16RequestCommand.START_TRANSACTION, error);
+    }
+  }
+
+  public async sendStopTransaction(transactionId: number, meterStop: number, idTag?: string, reason: OCPP16StopTransactionReason = OCPP16StopTransactionReason.NONE): Promise<OCPP16StopTransactionResponse> {
+    try {
+      const payload: StopTransactionRequest = {
+        transactionId,
+        ...!Utils.isUndefined(idTag) && { idTag },
+        meterStop: meterStop,
+        timestamp: new Date().toISOString(),
+        ...reason && { reason },
+      };
+      return await this.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, OCPP16RequestCommand.STOP_TRANSACTION) as OCPP16StartTransactionResponse;
+    } catch (error) {
+      this.handleRequestError(OCPP16RequestCommand.STOP_TRANSACTION, error);
+    }
+  }
+
+  // eslint-disable-next-line consistent-this
+  public async sendMeterValues(connectorId: number, transactionId: number, interval: number, self: OCPPRequestService, debug = false): Promise<void> {
+    try {
+      const meterValue: MeterValue = {
+        timestamp: new Date().toISOString(),
+        sampledValue: [],
+      };
+      const meterValuesTemplate: OCPP16SampledValue[] = self.chargingStation.getConnector(connectorId).MeterValues;
+      for (let index = 0; index < meterValuesTemplate.length; index++) {
+        const connector = self.chargingStation.getConnector(connectorId);
+        // SoC measurand
+        if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === OCPP16MeterValueMeasurand.STATE_OF_CHARGE && self.chargingStation.getConfigurationKey(OCPP16StandardParametersKey.MeterValuesSampledData).value.includes(OCPP16MeterValueMeasurand.STATE_OF_CHARGE)) {
+          meterValue.sampledValue.push({
+            ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.PERCENT },
+            ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
+            measurand: meterValuesTemplate[index].measurand,
+            ...!Utils.isUndefined(meterValuesTemplate[index].location) ? { location: meterValuesTemplate[index].location } : { location: MeterValueLocation.EV },
+            ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: Utils.getRandomInt(100).toString() },
+          });
+          const sampledValuesIndex = meterValue.sampledValue.length - 1;
+          if (Utils.convertToInt(meterValue.sampledValue[sampledValuesIndex].value) > 100 || debug) {
+            logger.error(`${self.chargingStation.logPrefix()} MeterValues measurand ${meterValue.sampledValue[sampledValuesIndex].measurand ? meterValue.sampledValue[sampledValuesIndex].measurand : OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${meterValue.sampledValue[sampledValuesIndex].value}/100`);
+          }
+        // Voltage measurand
+        } else if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === OCPP16MeterValueMeasurand.VOLTAGE && self.chargingStation.getConfigurationKey(OCPP16StandardParametersKey.MeterValuesSampledData).value.includes(OCPP16MeterValueMeasurand.VOLTAGE)) {
+          const voltageMeasurandValue = Utils.getRandomFloatRounded(self.chargingStation.getVoltageOut() + self.chargingStation.getVoltageOut() * 0.1, self.chargingStation.getVoltageOut() - self.chargingStation.getVoltageOut() * 0.1);
+          meterValue.sampledValue.push({
+            ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.VOLT },
+            ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
+            measurand: meterValuesTemplate[index].measurand,
+            ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
+            ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: voltageMeasurandValue.toString() },
+          });
+          for (let phase = 1; self.chargingStation.getNumberOfPhases() === 3 && phase <= self.chargingStation.getNumberOfPhases(); phase++) {
+            let phaseValue: string;
+            if (self.chargingStation.getVoltageOut() >= 0 && self.chargingStation.getVoltageOut() <= 250) {
+              phaseValue = `L${phase}-N`;
+            } else if (self.chargingStation.getVoltageOut() > 250) {
+              phaseValue = `L${phase}-L${(phase + 1) % self.chargingStation.getNumberOfPhases() !== 0 ? (phase + 1) % self.chargingStation.getNumberOfPhases() : self.chargingStation.getNumberOfPhases()}`;
+            }
+            meterValue.sampledValue.push({
+              ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.VOLT },
+              ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
+              measurand: meterValuesTemplate[index].measurand,
+              ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
+              ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: voltageMeasurandValue.toString() },
+              phase: phaseValue as MeterValuePhase,
+            });
+          }
+        // Power.Active.Import measurand
+        } else if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT && self.chargingStation.getConfigurationKey(OCPP16StandardParametersKey.MeterValuesSampledData).value.includes(OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT)) {
+          // FIXME: factor out powerDivider checks
+          if (Utils.isUndefined(self.chargingStation.stationInfo.powerDivider)) {
+            const errMsg = `${self.chargingStation.logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider is undefined`;
+            logger.error(errMsg);
+            throw Error(errMsg);
+          } else if (self.chargingStation.stationInfo.powerDivider && self.chargingStation.stationInfo.powerDivider <= 0) {
+            const errMsg = `${self.chargingStation.logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider have zero or below value ${self.chargingStation.stationInfo.powerDivider}`;
+            logger.error(errMsg);
+            throw Error(errMsg);
+          }
+          const errMsg = `${self.chargingStation.logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: Unknown ${self.chargingStation.getPowerOutType()} powerOutType in template file ${self.chargingStation.stationTemplateFile}, cannot calculate ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER} measurand value`;
+          const powerMeasurandValues = {} as MeasurandValues;
+          const maxPower = Math.round(self.chargingStation.stationInfo.maxPower / self.chargingStation.stationInfo.powerDivider);
+          const maxPowerPerPhase = Math.round((self.chargingStation.stationInfo.maxPower / self.chargingStation.stationInfo.powerDivider) / self.chargingStation.getNumberOfPhases());
+          switch (self.chargingStation.getPowerOutType()) {
+            case PowerOutType.AC:
+              if (Utils.isUndefined(meterValuesTemplate[index].value)) {
+                powerMeasurandValues.L1 = Utils.getRandomFloatRounded(maxPowerPerPhase);
+                powerMeasurandValues.L2 = 0;
+                powerMeasurandValues.L3 = 0;
+                if (self.chargingStation.getNumberOfPhases() === 3) {
+                  powerMeasurandValues.L2 = Utils.getRandomFloatRounded(maxPowerPerPhase);
+                  powerMeasurandValues.L3 = Utils.getRandomFloatRounded(maxPowerPerPhase);
+                }
+                powerMeasurandValues.allPhases = Utils.roundTo(powerMeasurandValues.L1 + powerMeasurandValues.L2 + powerMeasurandValues.L3, 2);
+              }
+              break;
+            case PowerOutType.DC:
+              if (Utils.isUndefined(meterValuesTemplate[index].value)) {
+                powerMeasurandValues.allPhases = Utils.getRandomFloatRounded(maxPower);
+              }
+              break;
+            default:
+              logger.error(errMsg);
+              throw Error(errMsg);
+          }
+          meterValue.sampledValue.push({
+            ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.WATT },
+            ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
+            measurand: meterValuesTemplate[index].measurand,
+            ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
+            ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: powerMeasurandValues.allPhases.toString() },
+          });
+          const sampledValuesIndex = meterValue.sampledValue.length - 1;
+          if (Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) > maxPower || debug) {
+            logger.error(`${self.chargingStation.logPrefix()} MeterValues measurand ${meterValue.sampledValue[sampledValuesIndex].measurand ? meterValue.sampledValue[sampledValuesIndex].measurand : OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${meterValue.sampledValue[sampledValuesIndex].value}/${maxPower}`);
+          }
+          for (let phase = 1; self.chargingStation.getNumberOfPhases() === 3 && phase <= self.chargingStation.getNumberOfPhases(); phase++) {
+            const phaseValue = `L${phase}-N`;
+            meterValue.sampledValue.push({
+              ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.WATT },
+              ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
+              ...!Utils.isUndefined(meterValuesTemplate[index].measurand) && { measurand: meterValuesTemplate[index].measurand },
+              ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
+              ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: powerMeasurandValues[`L${phase}`] as string },
+              phase: phaseValue as MeterValuePhase,
+            });
+          }
+        // Current.Import measurand
+        } else if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === OCPP16MeterValueMeasurand.CURRENT_IMPORT && self.chargingStation.getConfigurationKey(OCPP16StandardParametersKey.MeterValuesSampledData).value.includes(OCPP16MeterValueMeasurand.CURRENT_IMPORT)) {
+          // FIXME: factor out powerDivider checks
+          if (Utils.isUndefined(self.chargingStation.stationInfo.powerDivider)) {
+            const errMsg = `${self.chargingStation.logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider is undefined`;
+            logger.error(errMsg);
+            throw Error(errMsg);
+          } else if (self.chargingStation.stationInfo.powerDivider && self.chargingStation.stationInfo.powerDivider <= 0) {
+            const errMsg = `${self.chargingStation.logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider have zero or below value ${self.chargingStation.stationInfo.powerDivider}`;
+            logger.error(errMsg);
+            throw Error(errMsg);
+          }
+          const errMsg = `${self.chargingStation.logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: Unknown ${self.chargingStation.getPowerOutType()} powerOutType in template file ${self.chargingStation.stationTemplateFile}, cannot calculate ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER} measurand value`;
+          const currentMeasurandValues: MeasurandValues = {} as MeasurandValues;
+          let maxAmperage: number;
+          switch (self.chargingStation.getPowerOutType()) {
+            case PowerOutType.AC:
+              maxAmperage = ElectricUtils.ampPerPhaseFromPower(self.chargingStation.getNumberOfPhases(), self.chargingStation.stationInfo.maxPower / self.chargingStation.stationInfo.powerDivider, self.chargingStation.getVoltageOut());
+              if (Utils.isUndefined(meterValuesTemplate[index].value)) {
+                currentMeasurandValues.L1 = Utils.getRandomFloatRounded(maxAmperage);
+                currentMeasurandValues.L2 = 0;
+                currentMeasurandValues.L3 = 0;
+                if (self.chargingStation.getNumberOfPhases() === 3) {
+                  currentMeasurandValues.L2 = Utils.getRandomFloatRounded(maxAmperage);
+                  currentMeasurandValues.L3 = Utils.getRandomFloatRounded(maxAmperage);
+                }
+                currentMeasurandValues.allPhases = Utils.roundTo((currentMeasurandValues.L1 + currentMeasurandValues.L2 + currentMeasurandValues.L3) / self.chargingStation.getNumberOfPhases(), 2);
+              }
+              break;
+            case PowerOutType.DC:
+              maxAmperage = ElectricUtils.ampTotalFromPower(self.chargingStation.stationInfo.maxPower / self.chargingStation.stationInfo.powerDivider, self.chargingStation.getVoltageOut());
+              if (Utils.isUndefined(meterValuesTemplate[index].value)) {
+                currentMeasurandValues.allPhases = Utils.getRandomFloatRounded(maxAmperage);
+              }
+              break;
+            default:
+              logger.error(errMsg);
+              throw Error(errMsg);
+          }
+          meterValue.sampledValue.push({
+            ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.AMP },
+            ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
+            measurand: meterValuesTemplate[index].measurand,
+            ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
+            ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: currentMeasurandValues.allPhases.toString() },
+          });
+          const sampledValuesIndex = meterValue.sampledValue.length - 1;
+          if (Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) > maxAmperage || debug) {
+            logger.error(`${self.chargingStation.logPrefix()} MeterValues measurand ${meterValue.sampledValue[sampledValuesIndex].measurand ? meterValue.sampledValue[sampledValuesIndex].measurand : OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${meterValue.sampledValue[sampledValuesIndex].value}/${maxAmperage}`);
+          }
+          for (let phase = 1; self.chargingStation.getNumberOfPhases() === 3 && phase <= self.chargingStation.getNumberOfPhases(); phase++) {
+            const phaseValue = `L${phase}`;
+            meterValue.sampledValue.push({
+              ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.AMP },
+              ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
+              ...!Utils.isUndefined(meterValuesTemplate[index].measurand) && { measurand: meterValuesTemplate[index].measurand },
+              ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
+              ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: currentMeasurandValues[phaseValue] as string },
+              phase: phaseValue as MeterValuePhase,
+            });
+          }
+        // Energy.Active.Import.Register measurand (default)
+        } else if (!meterValuesTemplate[index].measurand || meterValuesTemplate[index].measurand === OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
+          // FIXME: factor out powerDivider checks
+          if (Utils.isUndefined(self.chargingStation.stationInfo.powerDivider)) {
+            const errMsg = `${self.chargingStation.logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider is undefined`;
+            logger.error(errMsg);
+            throw Error(errMsg);
+          } else if (self.chargingStation.stationInfo.powerDivider && self.chargingStation.stationInfo.powerDivider <= 0) {
+            const errMsg = `${self.chargingStation.logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider have zero or below value ${self.chargingStation.stationInfo.powerDivider}`;
+            logger.error(errMsg);
+            throw Error(errMsg);
+          }
+          if (Utils.isUndefined(meterValuesTemplate[index].value)) {
+            const measurandValue = Utils.getRandomInt(self.chargingStation.stationInfo.maxPower / (self.chargingStation.stationInfo.powerDivider * 3600000) * interval);
+            // Persist previous value in connector
+            if (connector && !Utils.isNullOrUndefined(connector.lastEnergyActiveImportRegisterValue) && connector.lastEnergyActiveImportRegisterValue >= 0) {
+              connector.lastEnergyActiveImportRegisterValue += measurandValue;
+            } else {
+              connector.lastEnergyActiveImportRegisterValue = 0;
+            }
+          }
+          meterValue.sampledValue.push({
+            ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.WATT_HOUR },
+            ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
+            ...!Utils.isUndefined(meterValuesTemplate[index].measurand) && { measurand: meterValuesTemplate[index].measurand },
+            ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
+            ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } :
+              { value: connector.lastEnergyActiveImportRegisterValue.toString() },
+          });
+          const sampledValuesIndex = meterValue.sampledValue.length - 1;
+          const maxConsumption = Math.round(self.chargingStation.stationInfo.maxPower * 3600 / (self.chargingStation.stationInfo.powerDivider * interval));
+          if (Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) > maxConsumption || debug) {
+            logger.error(`${self.chargingStation.logPrefix()} MeterValues measurand ${meterValue.sampledValue[sampledValuesIndex].measurand ? meterValue.sampledValue[sampledValuesIndex].measurand : OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${meterValue.sampledValue[sampledValuesIndex].value}/${maxConsumption}`);
+          }
+        // Unsupported measurand
+        } else {
+          logger.info(`${self.chargingStation.logPrefix()} Unsupported MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER} on connectorId ${connectorId}`);
+        }
+      }
+      const payload: MeterValuesRequest = {
+        connectorId,
+        transactionId,
+        meterValue,
+      };
+      await self.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, OCPP16RequestCommand.METERVALUES);
+    } catch (error) {
+      self.handleRequestError(OCPP16RequestCommand.METERVALUES, error);
+    }
+  }
+
+  public async sendError(messageId: string, error: OCPPError, commandName: OCPP16RequestCommand | OCPP16IncomingRequestCommand): Promise<unknown> {
+    // Send error
+    return this.sendMessage(messageId, error, MessageType.CALL_ERROR_MESSAGE, commandName);
+  }
+}
diff --git a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts
new file mode 100644 (file)
index 0000000..3180246
--- /dev/null
@@ -0,0 +1,119 @@
+import { AuthorizeRequest, OCPP16AuthorizationStatus, OCPP16AuthorizeResponse, OCPP16StartTransactionResponse, OCPP16StopTransactionResponse, StartTransactionRequest, StopTransactionRequest } from '../../../types/ocpp/1.6/Transaction';
+import { HeartbeatRequest, OCPP16BootNotificationRequest, OCPP16RequestCommand, StatusNotificationRequest } from '../../../types/ocpp/1.6/Requests';
+import { HeartbeatResponse, OCPP16BootNotificationResponse, OCPP16RegistrationStatus, StatusNotificationResponse } from '../../../types/ocpp/1.6/RequestResponses';
+import { MeterValuesRequest, MeterValuesResponse } from '../../../types/ocpp/1.6/MeterValues';
+
+import { OCPP16ChargePointStatus } from '../../../types/ocpp/1.6/ChargePointStatus';
+import { OCPP16StandardParametersKey } from '../../../types/ocpp/1.6/Configuration';
+import OCPPResponseService from '../OCPPResponseService';
+import Utils from '../../../utils/Utils';
+import logger from '../../../utils/Logger';
+
+export default class OCPP16ResponseService extends OCPPResponseService {
+  public async handleResponse(commandName: OCPP16RequestCommand, payload: Record<string, unknown> | string, requestPayload: Record<string, unknown>): Promise<void> {
+    const responseCallbackFn = 'handleResponse' + commandName;
+    if (typeof this[responseCallbackFn] === 'function') {
+      await this[responseCallbackFn](payload, requestPayload);
+    } else {
+      logger.error(this.chargingStation.logPrefix() + ' Trying to call an undefined response callback function: ' + responseCallbackFn);
+    }
+  }
+
+  private handleResponseBootNotification(payload: OCPP16BootNotificationResponse, requestPayload: OCPP16BootNotificationRequest): void {
+    if (payload.status === OCPP16RegistrationStatus.ACCEPTED) {
+      this.chargingStation.heartbeatSetInterval ? this.chargingStation.restartHeartbeat() : this.chargingStation.startHeartbeat();
+      this.chargingStation.addConfigurationKey(OCPP16StandardParametersKey.HeartBeatInterval, payload.interval.toString());
+      this.chargingStation.addConfigurationKey(OCPP16StandardParametersKey.HeartbeatInterval, payload.interval.toString(), false, false);
+    } else if (payload.status === OCPP16RegistrationStatus.PENDING) {
+      logger.info(this.chargingStation.logPrefix() + ' Charging station in pending state on the central server');
+    } else {
+      logger.info(this.chargingStation.logPrefix() + ' Charging station rejected by the central server');
+    }
+  }
+
+  private async handleResponseStartTransaction(payload: OCPP16StartTransactionResponse, requestPayload: StartTransactionRequest): Promise<void> {
+    const connectorId = requestPayload.connectorId;
+
+    let transactionConnectorId: number;
+    for (const connector in this.chargingStation.connectors) {
+      if (Utils.convertToInt(connector) > 0 && Utils.convertToInt(connector) === connectorId) {
+        transactionConnectorId = Utils.convertToInt(connector);
+        break;
+      }
+    }
+    if (!transactionConnectorId) {
+      logger.error(this.chargingStation.logPrefix() + ' Trying to start a transaction on a non existing connector Id ' + connectorId.toString());
+      return;
+    }
+    if (this.chargingStation.getConnector(connectorId)?.transactionStarted) {
+      logger.debug(this.chargingStation.logPrefix() + ' Trying to start a transaction on an already used connector ' + connectorId.toString() + ': %j', this.chargingStation.getConnector(connectorId));
+      return;
+    }
+
+    if (payload?.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
+      this.chargingStation.getConnector(connectorId).transactionStarted = true;
+      this.chargingStation.getConnector(connectorId).transactionId = payload.transactionId;
+      this.chargingStation.getConnector(connectorId).idTag = requestPayload.idTag;
+      this.chargingStation.getConnector(connectorId).lastEnergyActiveImportRegisterValue = 0;
+      await this.chargingStation.ocppRequestService.sendStatusNotification(connectorId, OCPP16ChargePointStatus.CHARGING);
+      this.chargingStation.getConnector(connectorId).status = OCPP16ChargePointStatus.CHARGING;
+      logger.info(this.chargingStation.logPrefix() + ' Transaction ' + payload.transactionId.toString() + ' STARTED on ' + this.chargingStation.stationInfo.chargingStationId + '#' + connectorId.toString() + ' for idTag ' + requestPayload.idTag);
+      if (this.chargingStation.stationInfo.powerSharedByConnectors) {
+        this.chargingStation.stationInfo.powerDivider++;
+      }
+      const configuredMeterValueSampleInterval = this.chargingStation.getConfigurationKey(OCPP16StandardParametersKey.MeterValueSampleInterval);
+      this.chargingStation.startMeterValues(connectorId, configuredMeterValueSampleInterval ? Utils.convertToInt(configuredMeterValueSampleInterval.value) * 1000 : 60000);
+    } else {
+      logger.error(this.chargingStation.logPrefix() + ' Starting transaction id ' + payload.transactionId.toString() + ' REJECTED with status ' + payload?.idTagInfo?.status + ', idTag ' + requestPayload.idTag);
+      this.chargingStation.resetTransactionOnConnector(connectorId);
+      await this.chargingStation.ocppRequestService.sendStatusNotification(connectorId, OCPP16ChargePointStatus.AVAILABLE);
+      this.chargingStation.getConnector(connectorId).status = OCPP16ChargePointStatus.AVAILABLE;
+    }
+  }
+
+  private async handleResponseStopTransaction(payload: OCPP16StopTransactionResponse, requestPayload: StopTransactionRequest): Promise<void> {
+    let transactionConnectorId: number;
+    for (const connector in this.chargingStation.connectors) {
+      if (Utils.convertToInt(connector) > 0 && this.chargingStation.getConnector(Utils.convertToInt(connector))?.transactionId === requestPayload.transactionId) {
+        transactionConnectorId = Utils.convertToInt(connector);
+        break;
+      }
+    }
+    if (!transactionConnectorId) {
+      logger.error(this.chargingStation.logPrefix() + ' Trying to stop a non existing transaction ' + requestPayload.transactionId.toString());
+      return;
+    }
+    if (payload.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
+      if (!this.chargingStation.isChargingStationAvailable() || !this.chargingStation.isConnectorAvailable(transactionConnectorId)) {
+        await this.chargingStation.ocppRequestService.sendStatusNotification(transactionConnectorId, OCPP16ChargePointStatus.UNAVAILABLE);
+        this.chargingStation.getConnector(transactionConnectorId).status = OCPP16ChargePointStatus.UNAVAILABLE;
+      } else {
+        await this.chargingStation.ocppRequestService.sendStatusNotification(transactionConnectorId, OCPP16ChargePointStatus.AVAILABLE);
+        this.chargingStation.getConnector(transactionConnectorId).status = OCPP16ChargePointStatus.AVAILABLE;
+      }
+      if (this.chargingStation.stationInfo.powerSharedByConnectors) {
+        this.chargingStation.stationInfo.powerDivider--;
+      }
+      logger.info(this.chargingStation.logPrefix() + ' Transaction ' + requestPayload.transactionId.toString() + ' STOPPED on ' + this.chargingStation.stationInfo.chargingStationId + '#' + transactionConnectorId.toString());
+      this.chargingStation.resetTransactionOnConnector(transactionConnectorId);
+    } else {
+      logger.error(this.chargingStation.logPrefix() + ' Stopping transaction id ' + requestPayload.transactionId.toString() + ' REJECTED with status ' + payload.idTagInfo?.status);
+    }
+  }
+
+  private handleResponseStatusNotification(payload: StatusNotificationRequest, requestPayload: StatusNotificationResponse): void {
+    logger.debug(this.chargingStation.logPrefix() + ' Status notification response received: %j to StatusNotification request: %j', payload, requestPayload);
+  }
+
+  private handleResponseMeterValues(payload: MeterValuesRequest, requestPayload: MeterValuesResponse): void {
+    logger.debug(this.chargingStation.logPrefix() + ' MeterValues response received: %j to MeterValues request: %j', payload, requestPayload);
+  }
+
+  private handleResponseHeartbeat(payload: HeartbeatResponse, requestPayload: HeartbeatRequest): void {
+    logger.debug(this.chargingStation.logPrefix() + ' Heartbeat response received: %j to Heartbeat request: %j', payload, requestPayload);
+  }
+
+  private handleResponseAuthorize(payload: OCPP16AuthorizeResponse, requestPayload: AuthorizeRequest): void {
+    logger.debug(this.chargingStation.logPrefix() + ' Authorize response received: %j to Authorize request: %j', payload, requestPayload);
+  }
+}
diff --git a/src/charging-station/ocpp/OCPPIncomingRequestService.ts b/src/charging-station/ocpp/OCPPIncomingRequestService.ts
new file mode 100644 (file)
index 0000000..897127b
--- /dev/null
@@ -0,0 +1,12 @@
+import ChargingStation from '../ChargingStation';
+import { IncomingRequestCommand } from '../../types/ocpp/Requests';
+
+export default abstract class OCPPIncomingRequestService {
+  protected chargingStation: ChargingStation;
+
+  constructor(chargingStation: ChargingStation) {
+    this.chargingStation = chargingStation;
+  }
+
+  public abstract handleRequest(messageId: string, commandName: IncomingRequestCommand, commandPayload: Record<string, unknown>): Promise<void>;
+}
diff --git a/src/charging-station/ocpp/OCPPRequestService.ts b/src/charging-station/ocpp/OCPPRequestService.ts
new file mode 100644 (file)
index 0000000..e09e15e
--- /dev/null
@@ -0,0 +1,121 @@
+import { AuthorizeResponse, StartTransactionResponse, StopTransactionReason, StopTransactionResponse } from '../../types/ocpp/Transaction';
+import { IncomingRequestCommand, Request, RequestCommand } from '../../types/ocpp/Requests';
+
+import { BootNotificationResponse } from '../../types/ocpp/RequestResponses';
+import { ChargePointErrorCode } from '../../types/ocpp/ChargePointErrorCode';
+import { ChargePointStatus } from '../../types/ocpp/ChargePointStatus';
+import ChargingStation from '../ChargingStation';
+import Constants from '../../utils/Constants';
+import { ErrorType } from '../../types/ocpp/ErrorType';
+import { MessageType } from '../../types/ocpp/MessageType';
+import OCPPError from '../OcppError';
+import OCPPResponseService from './OCPPResponseService';
+import logger from '../../utils/Logger';
+
+export default abstract class OCPPRequestService {
+  public chargingStation: ChargingStation;
+  protected ocppResponseService: OCPPResponseService;
+
+  constructor(chargingStation: ChargingStation, ocppResponseService: OCPPResponseService) {
+    this.chargingStation = chargingStation;
+    this.ocppResponseService = ocppResponseService;
+  }
+
+  public async sendMessage(messageId: string, commandParams: any, messageType: MessageType = MessageType.CALL_RESULT_MESSAGE, commandName: RequestCommand | IncomingRequestCommand): Promise<any> {
+    // eslint-disable-next-line @typescript-eslint/no-this-alias
+    const self = this;
+    const chargingStation = this.chargingStation;
+    // Send a message through wsConnection
+    return new Promise((resolve: (value?: any | PromiseLike<any>) => void, reject: (reason?: any) => void) => {
+      let messageToSend: string;
+      // Type of message
+      switch (messageType) {
+        // Request
+        case MessageType.CALL_MESSAGE:
+          // Build request
+          this.chargingStation.requests[messageId] = [responseCallback, rejectCallback, commandParams] as Request;
+          messageToSend = JSON.stringify([messageType, messageId, commandName, commandParams]);
+          break;
+        // Response
+        case MessageType.CALL_RESULT_MESSAGE:
+          // Build response
+          messageToSend = JSON.stringify([messageType, messageId, commandParams]);
+          break;
+        // Error Message
+        case MessageType.CALL_ERROR_MESSAGE:
+          // Build Error Message
+          messageToSend = JSON.stringify([messageType, messageId, commandParams.code ? commandParams.code : ErrorType.GENERIC_ERROR, commandParams.message ? commandParams.message : '', commandParams.details ? commandParams.details : {}]);
+          break;
+      }
+      // Check if wsConnection opened and charging station registered
+      if (this.chargingStation.isWebSocketOpen() && (this.chargingStation.isRegistered() || commandName === RequestCommand.BOOT_NOTIFICATION)) {
+        if (this.chargingStation.getEnableStatistics()) {
+          this.chargingStation.statistics.addMessage(commandName, messageType);
+        }
+        // Yes: Send Message
+        this.chargingStation.wsConnection.send(messageToSend);
+      } else if (commandName !== RequestCommand.BOOT_NOTIFICATION) {
+        let dups = false;
+        // Handle dups in buffer
+        for (const message of this.chargingStation.messageQueue) {
+          // Same message
+          if (messageToSend === message) {
+            dups = true;
+            break;
+          }
+        }
+        if (!dups) {
+          // Buffer message
+          this.chargingStation.messageQueue.push(messageToSend);
+        }
+        // Reject it
+        return rejectCallback(new OCPPError(commandParams.code ? commandParams.code : ErrorType.GENERIC_ERROR, commandParams.message ? commandParams.message : `WebSocket closed for message id '${messageId}' with content '${messageToSend}', message buffered`, commandParams.details ? commandParams.details : {}));
+      }
+      // Response?
+      if (messageType === MessageType.CALL_RESULT_MESSAGE) {
+        // Yes: send Ok
+        resolve();
+      } else if (messageType === MessageType.CALL_ERROR_MESSAGE) {
+        // Send timeout
+        setTimeout(() => rejectCallback(new OCPPError(commandParams.code ? commandParams.code : ErrorType.GENERIC_ERROR, commandParams.message ? commandParams.message : `Timeout for message id '${messageId}' with content '${messageToSend}'`, commandParams.details ? commandParams.details : {})), Constants.OCPP_ERROR_TIMEOUT);
+      }
+
+      // Function that will receive the request's response
+      async function responseCallback(payload: Record<string, unknown> | string, requestPayload: Record<string, unknown>): Promise<void> {
+        if (chargingStation.getEnableStatistics()) {
+          chargingStation.statistics.addMessage(commandName, messageType);
+        }
+        // Send the response
+        await self.ocppResponseService.handleResponse(commandName as RequestCommand, payload, requestPayload);
+        resolve(payload);
+      }
+
+      // Function that will receive the request's rejection
+      function rejectCallback(error: OCPPError): void {
+        if (chargingStation.getEnableStatistics()) {
+          chargingStation.statistics.addMessage(commandName, messageType);
+        }
+        logger.debug(`${chargingStation.logPrefix()} Error: %j occurred when calling command %s with parameters: %j`, error, commandName, commandParams);
+        // Build Exception
+        // eslint-disable-next-line no-empty-function
+        chargingStation.requests[messageId] = [() => { }, () => { }, {}]; // Properly format the request
+        // Send error
+        reject(error);
+      }
+    });
+  }
+
+  public handleRequestError(commandName: RequestCommand, error: Error): void {
+    logger.error(this.chargingStation.logPrefix() + ' Send ' + commandName + ' error: %j', error);
+    throw error;
+  }
+
+  public abstract sendHeartbeat(): Promise<void>;
+  public abstract sendBootNotification(chargePointModel: string, chargePointVendor: string, chargeBoxSerialNumber?: string, firmwareVersion?: string, chargePointSerialNumber?: string, iccid?: string, imsi?: string, meterSerialNumber?: string, meterType?: string): Promise<BootNotificationResponse>;
+  public abstract sendStatusNotification(connectorId: number, status: ChargePointStatus, errorCode?: ChargePointErrorCode): Promise<void>;
+  public abstract sendAuthorize(idTag?: string): Promise<AuthorizeResponse>;
+  public abstract sendStartTransaction(connectorId: number, idTag?: string): Promise<StartTransactionResponse>;
+  public abstract sendStopTransaction(transactionId: number, meterStop: number, idTag?: string, reason?: StopTransactionReason): Promise<StopTransactionResponse>;
+  public abstract sendMeterValues(connectorId: number, transactionId: number, interval: number, self: OCPPRequestService): Promise<void>;
+  public abstract sendError(messageId: string, error: OCPPError, commandName: RequestCommand | IncomingRequestCommand): Promise<unknown>;
+}
diff --git a/src/charging-station/ocpp/OCPPResponseService.ts b/src/charging-station/ocpp/OCPPResponseService.ts
new file mode 100644 (file)
index 0000000..0a1cd0c
--- /dev/null
@@ -0,0 +1,12 @@
+import ChargingStation from '../ChargingStation';
+import { RequestCommand } from '../../types/ocpp/Requests';
+
+export default abstract class OCPPResponseService {
+  protected chargingStation: ChargingStation;
+
+  constructor(chargingStation: ChargingStation) {
+    this.chargingStation = chargingStation;
+  }
+
+  public abstract handleResponse(commandName: RequestCommand, payload: Record<string, unknown> | string, requestPayload: Record<string, unknown>): Promise<void>;
+}
old mode 100644 (file)
new mode 100755 (executable)
similarity index 83%
rename from src/scripts/deleteChargingStations.ts
rename to src/scripts/deleteChargingStations.js
index 9a60277..0dbefc0
@@ -1,5 +1,7 @@
-import MongoClient from 'mongodb';
-import fs from 'fs';
+#!/usr/bin/env node
+
+const MongoClient = require('mongodb');
+const fs = require('fs');
 
 // This script deletes charging stations
 // Filter charging stations by id pattern
@@ -11,12 +13,12 @@ import fs from 'fs';
 const config = JSON.parse(fs.readFileSync('scriptConfig.json', 'utf8'));
 
 // Mongo Connection and Query
-if (config?.mongoConnectionString) {
+if (config && config.mongoConnectionString) {
   MongoClient.connect(config.mongoConnectionString, {
     useUnifiedTopology: true,
     useNewUrlParser: true
   }, async function(err, client) {
-    const db = client.db('evse');
+    const db = client.db();
 
     for await (const tenantID of config.tenantIDs) {
       const response = await db.collection(tenantID + '.chargingstations').deleteMany(
old mode 100644 (file)
new mode 100755 (executable)
similarity index 84%
rename from src/scripts/setCSPublicFlag.ts
rename to src/scripts/setCSPublicFlag.js
index 846160c..0650237
@@ -1,5 +1,7 @@
-import MongoClient from 'mongodb';
-import fs from 'fs';
+#!/usr/bin/env node
+
+const MongoClient = require('mongodb');
+const fs = require('fs');
 
 // This script sets charging stations public or private
 // Filter charging stations by id pattern
@@ -12,12 +14,12 @@ import fs from 'fs';
 const config = JSON.parse(fs.readFileSync('scriptConfig.json', 'utf8'));
 
 // Mongo Connection and Query
-if (config?.mongoConnectionString) {
+if (config && config.mongoConnectionString) {
   MongoClient.connect(config.mongoConnectionString, {
     useUnifiedTopology: true,
     useNewUrlParser: true
   }, async function(err, client) {
-    const db = client.db('evse');
+    const db = client.db();
 
     for await (const tenantID of config.tenantIDs) {
       const response = await db.collection(tenantID + '.chargingstations').updateMany(
index 03bd4b8b03d41cbb3fd7a0787f0e8f082a7770ba..56bafee7b86bebb53a6a97414183c544c8d72f58 100644 (file)
@@ -1,49 +1,6 @@
-import Configuration from './utils/Configuration';
-import { StationWorkerData } from './types/Worker';
-import Utils from './utils/Utils';
-import WorkerFactory from './worker/WorkerFactory';
-import Wrk from './worker/Wrk';
+import Bootstrap from './charging-station/Bootstrap';
 
-class Bootstrap {
-  static async start() {
-    try {
-      let numStationsTotal = 0;
-      const workerImplementation: Wrk = WorkerFactory.getWorkerImpl('./dist/charging-station/StationWorker.js');
-      await workerImplementation.start();
-      // Start ChargingStation object in worker thread
-      if (Configuration.getStationTemplateURLs()) {
-        for (const stationURL of Configuration.getStationTemplateURLs()) {
-          try {
-            const nbStations = stationURL.numberOfStations ? stationURL.numberOfStations : 0;
-            for (let index = 1; index <= nbStations; index++) {
-              const workerData: StationWorkerData = {
-                index,
-                templateFile: stationURL.file
-              };
-              await workerImplementation.addElement(workerData);
-              numStationsTotal++;
-            }
-          } catch (error) {
-            // eslint-disable-next-line no-console
-            console.error('Charging station start with template file ' + stationURL.file + ' error ', error);
-          }
-        }
-      } else {
-        console.log('No stationTemplateURLs defined in configuration, exiting');
-      }
-      if (numStationsTotal === 0) {
-        console.log('No charging station template enabled in configuration, exiting');
-      } else {
-        console.log(`Charging station simulator started with ${numStationsTotal.toString()} charging station(s) and ${Utils.workerDynamicPoolInUse() ? `${Configuration.getWorkerPoolMinSize().toString()}/` : ''}${workerImplementation.size}${Utils.workerPoolInUse() ? `/${Configuration.getWorkerPoolMaxSize().toString()}` : ''} worker(s) concurrently running in '${Configuration.getWorkerProcess()}' mode (${workerImplementation.maxElementsPerWorker} charging station(s) per worker)`);
-      }
-    } catch (error) {
-      // eslint-disable-next-line no-console
-      console.error('Bootstrap start error ', error);
-    }
-  }
-}
-
-Bootstrap.start().catch(
+Bootstrap.getInstance().start().catch(
   (error) => {
     console.error(error);
   }
index c97079d536802242aeda8835c68e0f7c07ab5148..b3b05b669099b40167f8a4bfbe0a0a7d8eafdee5 100644 (file)
@@ -1,5 +1,7 @@
 import ChargingStationConfiguration from './ChargingStationConfiguration';
 import Connectors from './Connectors';
+import { OCPPProtocol } from './ocpp/OCPPProtocol';
+import { OCPPVersion } from './ocpp/OCPPVersion';
 
 export enum PowerOutType {
   AC = 'AC',
@@ -31,6 +33,8 @@ export interface AutomaticTransactionGenerator {
 
 export default interface ChargingStationTemplate {
   supervisionURL?: string;
+  ocppVersion?: OCPPVersion;
+  ocppProtocol?: OCPPProtocol;
   authorizationFile?: string;
   baseName: string;
   nameSuffix?: string;
index 290a29a5561ad1f69ec07f5c13a9fbe1a86fb431..38bebf336dbdd2c16c6cc567e708bbed695bc8e7 100644 (file)
@@ -1,7 +1,7 @@
-import { AvailabilityType } from './ocpp/1.6/Requests';
-import { ChargePointStatus } from './ocpp/1.6/ChargePointStatus';
-import { ChargingProfile } from './ocpp/1.6/ChargingProfile';
-import { SampledValue } from './ocpp/1.6/MeterValues';
+import { AvailabilityType } from './ocpp/Requests';
+import { ChargePointStatus } from './ocpp/ChargePointStatus';
+import { ChargingProfile } from './ocpp/ChargingProfile';
+import { SampledValue } from './ocpp/MeterValues';
 
 export interface Connector {
   availability: AvailabilityType;
index 6ec9e9997ca1a17f33daa435eb0b34e43fd8bb02..bc2972684c0cb2677f34176ffbd11b119392d423 100644 (file)
@@ -6,6 +6,12 @@ export enum WorkerProcessType {
   STATIC_POOL = 'staticPool'
 }
 
+export interface WorkerOptions {
+  poolMaxSize?: number;
+  poolMinSize?: number;
+  elementsPerWorker?: number;
+}
+
 export interface WorkerData { }
 
 export interface StationWorkerData extends WorkerData {
index 5baaece468ba746f0e315fb5a1643cbbecaeada9..db06d0c9da53758e055398531fd549906b0b045a 100644 (file)
@@ -1,4 +1,4 @@
-export enum ChargePointErrorCode {
+export enum OCPP16ChargePointErrorCode {
   CONNECTOR_LOCK_FAILURE = 'ConnectorLockFailure',
   EV_COMMUNICATION_ERROR = 'EVCommunicationError',
   GROUND_FAILURE = 'GroundFailure',
index 45bad01d4c9d7a1abe30d38725e5b2f89433f6f2..272d757e958fec19b8f9610ec1b95e031baa6a19 100644 (file)
@@ -1,4 +1,4 @@
-export enum ChargePointStatus {
+export enum OCPP16ChargePointStatus {
   AVAILABLE = 'Available',
   PREPARING = 'Preparing',
   CHARGING = 'Charging',
index e4a66d786eb4a82fefd82b460832db616f3ac276..5d72a7ac1b28ec184de27ed45384510abb817096 100644 (file)
@@ -1,5 +1,4 @@
-
-export interface ChargingProfile {
+export interface OCPP16ChargingProfile {
   chargingProfileId: number;
   transactionId?: number;
   stackLevel: number;
index 941a854784f7c7757d0558f9939996c4da51996c..ae0c352d4a8d20ca690fcba53424010b45191dfa 100644 (file)
@@ -1,4 +1,4 @@
-export enum StandardParametersKey {
+export enum OCPP16StandardParametersKey {
   AllowOfflineTxForUnknownId = 'AllowOfflineTxForUnknownId',
   AuthorizationCacheEnabled = 'AuthorizationCacheEnabled',
   AuthorizeRemoteTxRequests = 'AuthorizeRemoteTxRequests',
index 47163ffc1d784208541b7885bdceda9818f8fd90..27d9cc3d9d36aa299eebeec487013178a4b61de4 100644 (file)
@@ -28,7 +28,7 @@ export enum MeterValueContext {
   TRIGGER = 'Trigger'
 }
 
-export enum MeterValueMeasurand {
+export enum OCPP16MeterValueMeasurand {
   CURRENT_EXPORT = 'Current.Export',
   CURRENT_IMPORT = 'Current.Import',
   CURRENT_OFFERED = 'Current.Offered',
@@ -79,11 +79,11 @@ export enum MeterValueFormat {
   SIGNED_DATA = 'SignedData',
 }
 
-export interface SampledValue {
+export interface OCPP16SampledValue {
   value?: string;
   unit?: MeterValueUnit;
   context?: MeterValueContext;
-  measurand?: MeterValueMeasurand;
+  measurand?: OCPP16MeterValueMeasurand;
   phase?: MeterValuePhase;
   location?: MeterValueLocation;
   format?: MeterValueFormat;
@@ -91,7 +91,7 @@ export interface SampledValue {
 
 export interface MeterValue {
   timestamp: string;
-  sampledValue: SampledValue[];
+  sampledValue: OCPP16SampledValue[];
 }
 
 export interface MeterValuesRequest {
@@ -101,5 +101,5 @@ export interface MeterValuesRequest {
 }
 
 // eslint-disable-next-line @typescript-eslint/no-empty-interface
-export interface MeterValuesResponse {}
+export interface MeterValuesResponse { }
 
index e8861ec946240f3c6fd69cbf17cc758b0ac05056..f40cc88ba00992d8110d98df3c3f16b168e74c55 100644 (file)
@@ -34,14 +34,14 @@ export interface ChangeConfigurationResponse {
   status: ConfigurationStatus;
 }
 
-export enum RegistrationStatus {
+export enum OCPP16RegistrationStatus {
   ACCEPTED = 'Accepted',
   PENDING = 'Pending',
   REJECTED = 'Rejected'
 }
 
-export interface BootNotificationResponse {
-  status: RegistrationStatus;
+export interface OCPP16BootNotificationResponse {
+  status: OCPP16RegistrationStatus;
   currentTime: string;
   interval: number;
 }
index 0602c2831910dbddef90decc2e3f176cc60d3e04..8af407c0ac3789fceb6ca52fb54c3cbec133398f 100644 (file)
@@ -1,10 +1,10 @@
-import { ChargingProfile, ChargingProfilePurposeType } from './ChargingProfile';
+import { ChargingProfilePurposeType, OCPP16ChargingProfile } from './ChargingProfile';
 
-import { ChargePointErrorCode } from './ChargePointErrorCode';
-import { ChargePointStatus } from './ChargePointStatus';
-import { StandardParametersKey } from './Configuration';
+import { OCPP16ChargePointErrorCode } from './ChargePointErrorCode';
+import { OCPP16ChargePointStatus } from './ChargePointStatus';
+import { OCPP16StandardParametersKey } from './Configuration';
 
-export enum RequestCommand {
+export enum OCPP16RequestCommand {
   BOOT_NOTIFICATION = 'BootNotification',
   HEARTBEAT = 'Heartbeat',
   STATUS_NOTIFICATION = 'StatusNotification',
@@ -15,7 +15,7 @@ export enum RequestCommand {
   METERVALUES = 'MeterValues'
 }
 
-export enum IncomingRequestCommand {
+export enum OCPP16IncomingRequestCommand {
   RESET = 'Reset',
   CLEAR_CACHE = 'ClearCache',
   CHANGE_AVAILABILITY = 'ChangeAvailability',
@@ -31,7 +31,7 @@ export enum IncomingRequestCommand {
 // eslint-disable-next-line @typescript-eslint/no-empty-interface
 export interface HeartbeatRequest { }
 
-export interface BootNotificationRequest {
+export interface OCPP16BootNotificationRequest {
   chargeBoxSerialNumber?: string;
   chargePointModel: string;
   chargePointSerialNumber?: string;
@@ -45,23 +45,23 @@ export interface BootNotificationRequest {
 
 export interface StatusNotificationRequest {
   connectorId: number;
-  errorCode: ChargePointErrorCode;
+  errorCode: OCPP16ChargePointErrorCode;
   info?: string;
-  status: ChargePointStatus;
+  status: OCPP16ChargePointStatus;
   timestamp?: string;
   vendorId?: string;
   vendorErrorCode?: string;
 }
 
 export interface ChangeConfigurationRequest {
-  key: string | StandardParametersKey;
+  key: string | OCPP16StandardParametersKey;
   value: string;
 }
 
 export interface RemoteStartTransactionRequest {
   connectorId: number;
   idTag: string;
-  chargingProfile?: ChargingProfile;
+  chargingProfile?: OCPP16ChargingProfile;
 }
 
 export interface RemoteStopTransactionRequest {
@@ -73,7 +73,7 @@ export interface UnlockConnectorRequest {
 }
 
 export interface GetConfigurationRequest {
-  key?: string | StandardParametersKey[];
+  key?: string | OCPP16StandardParametersKey[];
 }
 
 export enum ResetType {
@@ -87,17 +87,17 @@ export interface ResetRequest {
 
 export interface SetChargingProfileRequest {
   connectorId: number;
-  csChargingProfiles: ChargingProfile;
+  csChargingProfiles: OCPP16ChargingProfile;
 }
 
-export enum AvailabilityType {
+export enum OCPP16AvailabilityType {
   INOPERATIVE = 'Inoperative',
   OPERATIVE = 'Operative'
 }
 
 export interface ChangeAvailabilityRequest {
   connectorId: number;
-  type: AvailabilityType;
+  type: OCPP16AvailabilityType;
 }
 
 export interface ClearChargingProfileRequest {
index 22fb6080efadc697771178715dbd86b69cf81ae2..69aff0e0115b3849bbf9e8b1a8ced5fe6536a8f3 100644 (file)
@@ -1,6 +1,6 @@
 import { MeterValue } from './MeterValues';
 
-export enum StopTransactionReason {
+export enum OCPP16StopTransactionReason {
   NONE = '',
   EMERGENCY_STOP = 'EmergencyStop',
   EV_DISCONNECTED = 'EVDisconnected',
@@ -15,7 +15,7 @@ export enum StopTransactionReason {
   DE_AUTHORIZED = 'DeAuthorized'
 }
 
-export enum AuthorizationStatus {
+export enum OCPP16AuthorizationStatus {
   ACCEPTED = 'Accepted',
   BLOCKED = 'Blocked',
   EXPIRED = 'Expired',
@@ -24,7 +24,7 @@ export enum AuthorizationStatus {
 }
 
 export interface IdTagInfo {
-  status: AuthorizationStatus;
+  status: OCPP16AuthorizationStatus;
   parentIdTag?: string;
   expiryDate?: Date;
 }
@@ -33,7 +33,7 @@ export interface AuthorizeRequest {
   idTag: string;
 
 }
-export interface AuthorizeResponse {
+export interface OCPP16AuthorizeResponse {
   idTagInfo: IdTagInfo;
 }
 
@@ -45,7 +45,7 @@ export interface StartTransactionRequest {
   timestamp: string;
 }
 
-export interface StartTransactionResponse {
+export interface OCPP16StartTransactionResponse {
   idTagInfo: IdTagInfo;
   transactionId: number;
 }
@@ -55,10 +55,10 @@ export interface StopTransactionRequest {
   meterStop: number;
   timestamp: string;
   transactionId: number;
-  reason?: StopTransactionReason;
+  reason?: OCPP16StopTransactionReason;
   transactionData?: MeterValue[];
 }
 
-export interface StopTransactionResponse {
+export interface OCPP16StopTransactionResponse {
   idTagInfo?: IdTagInfo;
 }
diff --git a/src/types/ocpp/ChargePointErrorCode.ts b/src/types/ocpp/ChargePointErrorCode.ts
new file mode 100644 (file)
index 0000000..52442e5
--- /dev/null
@@ -0,0 +1,3 @@
+import { OCPP16ChargePointErrorCode } from './1.6/ChargePointErrorCode';
+
+export type ChargePointErrorCode = OCPP16ChargePointErrorCode;
diff --git a/src/types/ocpp/ChargePointStatus.ts b/src/types/ocpp/ChargePointStatus.ts
new file mode 100644 (file)
index 0000000..daa9779
--- /dev/null
@@ -0,0 +1,7 @@
+import { OCPP16ChargePointStatus } from './1.6/ChargePointStatus';
+
+export type ChargePointStatus = OCPP16ChargePointStatus;
+
+export const ChargePointStatus = {
+  ...OCPP16ChargePointStatus
+};
diff --git a/src/types/ocpp/ChargingProfile.ts b/src/types/ocpp/ChargingProfile.ts
new file mode 100644 (file)
index 0000000..6dab7cf
--- /dev/null
@@ -0,0 +1,3 @@
+import { OCPP16ChargingProfile } from './1.6/ChargingProfile';
+
+export type ChargingProfile = OCPP16ChargingProfile;
index 57b3f90f150309c7fb21b58d10ce96566fd9084c..f486532574c4a9355b6b95f137f18c3e825ce564 100644 (file)
@@ -1,4 +1,10 @@
-import { StandardParametersKey } from './1.6/Configuration';
+import { OCPP16StandardParametersKey } from './1.6/Configuration';
+
+export type StandardParametersKey = OCPP16StandardParametersKey;
+
+export const StandardParametersKey = {
+  ...OCPP16StandardParametersKey
+};
 
 export interface OCPPConfigurationKey {
   key: string | StandardParametersKey;
diff --git a/src/types/ocpp/MeterValues.ts b/src/types/ocpp/MeterValues.ts
new file mode 100644 (file)
index 0000000..b052ce6
--- /dev/null
@@ -0,0 +1,9 @@
+import { OCPP16MeterValueMeasurand, OCPP16SampledValue } from './1.6/MeterValues';
+
+export type MeterValueMeasurand = OCPP16MeterValueMeasurand;
+
+export const MeterValueMeasurand = {
+  ...OCPP16MeterValueMeasurand
+};
+
+export type SampledValue = OCPP16SampledValue;
diff --git a/src/types/ocpp/OCPPProtocol.ts b/src/types/ocpp/OCPPProtocol.ts
new file mode 100644 (file)
index 0000000..79291e6
--- /dev/null
@@ -0,0 +1,4 @@
+export enum OCPPProtocol {
+  SOAP = 'soap',
+  JSON = 'json',
+}
diff --git a/src/types/ocpp/OCPPVersion.ts b/src/types/ocpp/OCPPVersion.ts
new file mode 100644 (file)
index 0000000..85c6155
--- /dev/null
@@ -0,0 +1,6 @@
+export enum OCPPVersion {
+  VERSION_12 = '1.2',
+  VERSION_15 = '1.5',
+  VERSION_16 = '1.6',
+  VERSION_20 = '2.0',
+}
diff --git a/src/types/ocpp/RequestResponses.ts b/src/types/ocpp/RequestResponses.ts
new file mode 100644 (file)
index 0000000..08b1080
--- /dev/null
@@ -0,0 +1,9 @@
+import { OCPP16BootNotificationResponse, OCPP16RegistrationStatus } from './1.6/RequestResponses';
+
+export type BootNotificationResponse = OCPP16BootNotificationResponse;
+
+export type RegistrationStatus = OCPP16RegistrationStatus;
+
+export const RegistrationStatus = {
+  ...OCPP16RegistrationStatus
+};
index ea2da2436ddf00d66d85309b3c229030a3aef56b..86838d221c636b6f98f59860f1c42b0ae552e2d7 100644 (file)
@@ -1,4 +1,5 @@
-import { IncomingRequestCommand } from './1.6/Requests';
+import { OCPP16AvailabilityType, OCPP16BootNotificationRequest, OCPP16IncomingRequestCommand, OCPP16RequestCommand } from './1.6/Requests';
+
 import { MessageType } from './MessageType';
 import OCPPError from '../../charging-station/OcppError';
 
@@ -6,6 +7,22 @@ export default interface Requests {
   [id: string]: Request;
 }
 
+export type BootNotificationRequest = OCPP16BootNotificationRequest;
+
+export type AvailabilityType = OCPP16AvailabilityType;
+
+export const AvailabilityType = {
+  ...OCPP16AvailabilityType
+};
+
+export type RequestCommand = OCPP16RequestCommand;
+
+export const RequestCommand = {
+  ...OCPP16RequestCommand
+};
+
+export type IncomingRequestCommand = OCPP16IncomingRequestCommand;
+
 export type Request = [(payload?: Record<string, unknown>, requestPayload?: Record<string, unknown>) => void, (error?: OCPPError) => void, Record<string, unknown>];
 
 export type IncomingRequest = [MessageType, string, IncomingRequestCommand, Record<string, unknown>, Record<string, unknown>];
diff --git a/src/types/ocpp/Transaction.ts b/src/types/ocpp/Transaction.ts
new file mode 100644 (file)
index 0000000..83764a2
--- /dev/null
@@ -0,0 +1,19 @@
+import { OCPP16AuthorizationStatus, OCPP16AuthorizeResponse, OCPP16StartTransactionResponse, OCPP16StopTransactionReason, OCPP16StopTransactionResponse } from './1.6/Transaction';
+
+export type AuthorizationStatus = OCPP16AuthorizationStatus;
+
+export const AuthorizationStatus = {
+  ...OCPP16AuthorizationStatus,
+};
+
+export type AuthorizeResponse = OCPP16AuthorizeResponse;
+
+export type StopTransactionReason = OCPP16StopTransactionReason;
+
+export const StopTransactionReason = {
+  ...OCPP16StopTransactionReason,
+};
+
+export type StartTransactionResponse = OCPP16StartTransactionResponse;
+
+export type StopTransactionResponse = OCPP16StopTransactionResponse;
index ba3eba19231ab7b7dff4eef88e27461ecc29c7f5..535fb00e5cd34c811d03e96125e608d4ae6693d3 100644 (file)
@@ -1,6 +1,6 @@
 
 export default class CircularArray<T> extends Array<T> {
-  public size: number;
+  size: number;
   private readonly maximumCircularArraySize = 2000;
 
   constructor(size?: number) {
index e27ec1276539a2d7b32decec68ff9f3e6a87e39e..6d1f7ccfd06ee85df55e8d2fccfeb5f33c18c9c7 100644 (file)
@@ -1,9 +1,12 @@
 import ConfigurationData, { StationTemplateURL } from '../types/ConfigurationData';
 
+import Bootstrap from '../charging-station/Bootstrap';
 import { WorkerProcessType } from '../types/Worker';
 import fs from 'fs';
 
 export default class Configuration {
+  private static configurationFilePath = './src/assets/config.json';
+  private static configurationFileWatcher: fs.FSWatcher;
   private static configuration: ConfigurationData;
 
   static getStatisticsDisplayInterval(): number {
@@ -105,11 +108,23 @@ export default class Configuration {
   // Read the config file
   private static getConfig(): ConfigurationData {
     if (!Configuration.configuration) {
-      Configuration.configuration = JSON.parse(fs.readFileSync('./src/assets/config.json', 'utf8')) as ConfigurationData;
+      Configuration.configuration = JSON.parse(fs.readFileSync(Configuration.configurationFilePath, 'utf8')) as ConfigurationData;
+      if (!Configuration.configurationFileWatcher) {
+        Configuration.configurationFileWatcher = Configuration.getConfigurationFileWatcher();
+      }
     }
     return Configuration.configuration;
   }
 
+  private static getConfigurationFileWatcher(): fs.FSWatcher {
+    // eslint-disable-next-line @typescript-eslint/no-misused-promises
+    return fs.watch(Configuration.configurationFilePath).on('change', async (e): Promise<void> => {
+      // Nullify to force configuration file reading
+      Configuration.configuration = null;
+      await Bootstrap.getInstance().restart();
+    });
+  }
+
   private static objectHasOwnProperty(object: any, property: string): boolean {
     return Object.prototype.hasOwnProperty.call(object, property) as boolean;
   }
index 35a77a69369a5220eabfbec4a90fcb7746ef8fe1..31a673281c8b9a43a2d37b405b611ce0a2c04588 100644 (file)
@@ -22,18 +22,12 @@ export default class Constants {
   static readonly OCPP_AVAILABILITY_RESPONSE_REJECTED = Object.freeze({ status: AvailabilityStatus.REJECTED });
   static readonly OCPP_AVAILABILITY_RESPONSE_SCHEDULED = Object.freeze({ status: AvailabilityStatus.SCHEDULED });
 
-  static readonly OCPP_PROTOCOL_JSON = 'json';
-  static readonly OCPP_PROTOCOL_SOAP = 'soap';
-  static readonly OCPP_VERSION_12 = '1.2';
-  static readonly OCPP_VERSION_15 = '1.5';
-  static readonly OCPP_VERSION_16 = '1.6';
-  static readonly OCPP_VERSION_20 = '2.0';
-
   static readonly OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL = 60000; // Ms
   static readonly OCPP_ERROR_TIMEOUT = 60000; // 60 sec
 
   static readonly CHARGING_STATION_DEFAULT_RESET_TIME = 60000; // Ms
   static readonly CHARGING_STATION_ATG_WAIT_TIME = 2000; // Ms
+  static readonly CHARGING_STATION_ATG_INITIALIZATION_TIME = 1000; // Ms
 
   static readonly TRANSACTION_DEFAULT_TAGID = '00000000';
 
index 966f1b57a6cc2b61de80485bc32c7cb57abbb5c2..d194451a031e5b2a3078f2107f8211ffa8f76de9 100644 (file)
@@ -1,5 +1,5 @@
 import CommandStatistics, { CommandStatisticsData, PerfEntry } from '../types/CommandStatistics';
-import { IncomingRequestCommand, RequestCommand } from '../types/ocpp/1.6/Requests';
+import { IncomingRequestCommand, RequestCommand } from '../types/ocpp/Requests';
 
 import CircularArray from './CircularArray';
 import Configuration from './Configuration';
@@ -12,9 +12,9 @@ export default class Statistics {
   private objId: string;
   private commandsStatistics: CommandStatistics;
 
-  public constructor(objName: string) {
-    this.objId = objName;
-    this.commandsStatistics = { id: this.objId ? this.objId : ' Object id not specified', commandsStatisticsData: {} } as CommandStatistics;
+  public constructor(objId: string) {
+    this.objId = objId;
+    this.commandsStatistics = { id: this.objId ? this.objId : 'Object id not specified', commandsStatisticsData: {} };
   }
 
   public addMessage(command: RequestCommand | IncomingRequestCommand, messageType: MessageType): void {
@@ -52,7 +52,7 @@ export default class Statistics {
         }
         break;
       default:
-        logger.error(`${this._logPrefix()} Wrong message type ${messageType}`);
+        logger.error(`${this.logPrefix()} Wrong message type ${messageType}`);
         break;
     }
   }
@@ -64,23 +64,23 @@ export default class Statistics {
     perfEntry.entryType = entry.entryType;
     perfEntry.startTime = entry.startTime;
     perfEntry.duration = entry.duration;
-    logger.info(`${this._logPrefix()} object ${className} method(s) performance entry: %j`, perfEntry);
+    logger.info(`${this.logPrefix()} object ${className} method(s) performance entry: %j`, perfEntry);
   }
 
   public start(): void {
-    this._displayInterval();
+    this.displayInterval();
   }
 
-  private _display(): void {
-    logger.info(this._logPrefix() + ' %j', this.commandsStatistics);
+  private display(): void {
+    logger.info(this.logPrefix() + ' %j', this.commandsStatistics);
   }
 
-  private _displayInterval(): void {
+  private displayInterval(): void {
     if (Configuration.getStatisticsDisplayInterval() > 0) {
       setInterval(() => {
-        this._display();
+        this.display();
       }, Configuration.getStatisticsDisplayInterval() * 1000);
-      logger.info(this._logPrefix() + ' displayed every ' + Utils.secondsToHHMMSS(Configuration.getStatisticsDisplayInterval()));
+      logger.info(this.logPrefix() + ' displayed every ' + Utils.secondsToHHMMSS(Configuration.getStatisticsDisplayInterval()));
     }
   }
 
@@ -121,7 +121,7 @@ export default class Statistics {
     this.commandsStatistics.commandsStatisticsData[command].medTimeMeasurement = this.median(this.commandsStatistics.commandsStatisticsData[command].timeMeasurementSeries);
   }
 
-  private _logPrefix(): string {
+  private logPrefix(): string {
     return Utils.logPrefix(` ${this.objId} Statistics:`);
   }
 }
index 570e5d1bcb2bded02486202787a876d32f251d88..aa3542b26386f72e75c39eb589656e3f9009e1ca 100644 (file)
@@ -5,7 +5,7 @@ import Utils from '../utils/Utils';
 import { WorkerData } from '../types/Worker';
 import Wrk from './Wrk';
 
-export default class WorkerDynamicPool extends Wrk {
+export default class WorkerDynamicPool<T> extends Wrk {
   private pool: DynamicPool;
 
   /**
@@ -39,7 +39,16 @@ export default class WorkerDynamicPool extends Wrk {
    * @return {Promise<void>}
    * @public
    */
-  public async addElement(elementData: WorkerData): Promise<void> {
+  public async stop(): Promise<void> {
+    return this.pool.destroy();
+  }
+
+  /**
+   *
+   * @return {Promise<void>}
+   * @public
+   */
+  public async addElement(elementData: T): Promise<void> {
     await this.pool.execute(elementData);
     // Start worker sequentially to optimize memory at startup
     await Utils.sleep(Constants.START_WORKER_DELAY);
index 2d923daab084bb95b77a783509053bed4b519a7a..dba59173510a3e63e33c7dafa191d444eb406385 100644 (file)
@@ -1,19 +1,39 @@
-import Configuration from '../utils/Configuration';
+import { WorkerOptions, WorkerProcessType } from '../types/Worker';
+
+import Utils from '../utils/Utils';
 import WorkerDynamicPool from './WorkerDynamicPool';
-import { WorkerProcessType } from '../types/Worker';
 import WorkerSet from './WorkerSet';
 import WorkerStaticPool from './WorkerStaticPool';
 import Wrk from './Wrk';
+import { isMainThread } from 'worker_threads';
 
 export default class WorkerFactory {
-  public static getWorkerImpl(workerScript: string): Wrk {
-    switch (Configuration.getWorkerProcess()) {
+  public static getWorkerImplementation<T>(workerScript: string, workerProcessType: WorkerProcessType, options?: WorkerOptions): Wrk {
+    if (!isMainThread) {
+      throw new Error('Trying to get a worker implementation outside the main thread');
+    }
+    if (Utils.isUndefined(options)) {
+      options = {} as WorkerOptions;
+    }
+    switch (workerProcessType) {
       case WorkerProcessType.WORKER_SET:
-        return new WorkerSet(workerScript, Configuration.getChargingStationsPerWorker());
+        if (Utils.isUndefined(options.elementsPerWorker)) {
+          options.elementsPerWorker = 1;
+        }
+        return new WorkerSet<T>(workerScript, options.elementsPerWorker);
       case WorkerProcessType.STATIC_POOL:
-        return new WorkerStaticPool(workerScript, Configuration.getWorkerPoolMaxSize());
+        if (Utils.isUndefined(options.poolMaxSize)) {
+          options.poolMaxSize = 16;
+        }
+        return new WorkerStaticPool<T>(workerScript, options.poolMaxSize);
       case WorkerProcessType.DYNAMIC_POOL:
-        return new WorkerDynamicPool(workerScript, Configuration.getWorkerPoolMinSize(), Configuration.getWorkerPoolMaxSize());
+        if (Utils.isUndefined(options.poolMinSize)) {
+          options.poolMinSize = 4;
+        }
+        if (Utils.isUndefined(options.poolMaxSize)) {
+          options.poolMaxSize = 16;
+        }
+        return new WorkerDynamicPool<T>(workerScript, options.poolMinSize, options.poolMaxSize);
       default:
         return null;
     }
index 0c59c09108089d059184bcaac99e6ffc104c3341..c550149b71b5945e50058b8a6f27c1d72bf88275 100644 (file)
@@ -1,13 +1,13 @@
-import { WorkerData, WorkerEvents, WorkerSetElement } from '../types/Worker';
+import { WorkerEvents, WorkerSetElement } from '../types/Worker';
 
 import Constants from '../utils/Constants';
 import Utils from '../utils/Utils';
 import { Worker } from 'worker_threads';
 import Wrk from './Wrk';
 
-export default class WorkerSet extends Wrk {
+export default class WorkerSet<T> extends Wrk {
   public maxElementsPerWorker: number;
-  private workers: Set<WorkerSetElement>;
+  private workerSet: Set<WorkerSetElement>;
 
   /**
    * Create a new `WorkerSet`.
@@ -17,12 +17,12 @@ export default class WorkerSet extends Wrk {
    */
   constructor(workerScript: string, maxElementsPerWorker = 1) {
     super(workerScript);
-    this.workers = new Set<WorkerSetElement>();
+    this.workerSet = new Set<WorkerSetElement>();
     this.maxElementsPerWorker = maxElementsPerWorker;
   }
 
   get size(): number {
-    return this.workers.size;
+    return this.workerSet.size;
   }
 
   /**
@@ -30,8 +30,8 @@ export default class WorkerSet extends Wrk {
    * @return {Promise<void>}
    * @public
    */
-  public async addElement(elementData: WorkerData): Promise<void> {
-    if (!this.workers) {
+  public async addElement(elementData: T): Promise<void> {
+    if (!this.workerSet) {
       throw Error('Cannot add a WorkerSet element: workers\' set does not exist');
     }
     if (this.getLastWorkerSetElement().numberOfWorkerElements >= this.maxElementsPerWorker) {
@@ -54,6 +54,18 @@ export default class WorkerSet extends Wrk {
     await Utils.sleep(Constants.START_WORKER_DELAY);
   }
 
+  /**
+   *
+   * @return {Promise<void>}
+   * @public
+   */
+  public async stop(): Promise<void> {
+    for (const workerSetElement of this.workerSet) {
+      await workerSetElement.worker.terminate();
+    }
+    this.workerSet.clear();
+  }
+
   /**
    *
    * @return {Promise}
@@ -69,13 +81,13 @@ export default class WorkerSet extends Wrk {
       }
       // FIXME: remove matching worker set element
     });
-    this.workers.add({ worker, numberOfWorkerElements: 0 });
+    this.workerSet.add({ worker, numberOfWorkerElements: 0 });
   }
 
   private getLastWorkerSetElement(): WorkerSetElement {
     let workerSetElement: WorkerSetElement;
     // eslint-disable-next-line no-empty
-    for (workerSetElement of this.workers) { }
+    for (workerSetElement of this.workerSet) { }
     return workerSetElement;
   }
 
@@ -85,7 +97,7 @@ export default class WorkerSet extends Wrk {
 
   private getWorkerSetElementByWorker(worker: Worker): WorkerSetElement {
     let workerSetElt: WorkerSetElement;
-    this.workers.forEach((workerSetElement) => {
+    this.workerSet.forEach((workerSetElement) => {
       if (JSON.stringify(workerSetElement.worker) === JSON.stringify(worker)) {
         workerSetElt = workerSetElement;
       }
index 9c2bc41d4adac0d905a4603e3adfd9c57187a646..1de7cba23304a7f3812dd906a9c64162a3f39dc9 100644 (file)
@@ -5,7 +5,7 @@ import Utils from '../utils/Utils';
 import { WorkerData } from '../types/Worker';
 import Wrk from './Wrk';
 
-export default class WorkerStaticPool extends Wrk {
+export default class WorkerStaticPool<T> extends Wrk {
   private pool: StaticPool;
 
   /**
@@ -39,7 +39,16 @@ export default class WorkerStaticPool extends Wrk {
    * @return {Promise<void>}
    * @public
    */
-  public async addElement(elementData: WorkerData): Promise<void> {
+  public async stop(): Promise<void> {
+    return this.pool.destroy();
+  }
+
+  /**
+   *
+   * @return {Promise<void>}
+   * @public
+   */
+  public async addElement(elementData: T): Promise<void> {
     await this.pool.execute(elementData);
     // Start worker sequentially to optimize memory at startup
     await Utils.sleep(Constants.START_WORKER_DELAY);
index 02589a593d7a7ecbf5bb416caa1168d0f67b2eec..d29494a8f33282c1370234b247014622b4f2c279 100644 (file)
@@ -15,5 +15,6 @@ export default abstract class Wrk {
   }
 
   public abstract start(): Promise<void>;
+  public abstract stop(): Promise<void>;
   public abstract addElement(elementData: WorkerData): Promise<void>;
 }