build: bump volta pnpm version
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
index bd3fb81283b079f66ceed55b380434666c331613..bcd33ccf0ca17b7f595b2d4f9d38fd6299fc7061 100644 (file)
@@ -1,6 +1,7 @@
 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
 
 import { createHash } from 'node:crypto';
+import { EventEmitter } from 'node:events';
 import { type FSWatcher, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
 import { dirname, join } from 'node:path';
 import { URL } from 'node:url';
@@ -67,6 +68,7 @@ import {
   type BootNotificationResponse,
   type CachedRequest,
   type ChargingStationConfiguration,
+  ChargingStationEvents,
   type ChargingStationInfo,
   type ChargingStationOcppConfiguration,
   type ChargingStationTemplate,
@@ -149,13 +151,12 @@ import {
   roundTo,
   secureRandom,
   sleep,
-  // watchJsonFile,
+  watchJsonFile,
 } from '../utils';
 
-export class ChargingStation {
+export class ChargingStation extends EventEmitter {
   public readonly index: number;
   public readonly templateFile: string;
-  public stationInfo!: ChargingStationInfo;
   public started: boolean;
   public starting: boolean;
   public idTagsCache: IdTagsCache;
@@ -171,6 +172,7 @@ export class ChargingStation {
   public bootNotificationRequest!: BootNotificationRequest;
   public bootNotificationResponse!: BootNotificationResponse | undefined;
   public powerDivider!: number;
+  private internalStationInfo!: ChargingStationInfo;
   private stopping: boolean;
   private configurationFile!: string;
   private configurationFileHash!: string;
@@ -190,6 +192,7 @@ export class ChargingStation {
   private reservationExpirationSetInterval?: NodeJS.Timeout;
 
   constructor(index: number, templateFile: string) {
+    super();
     this.started = false;
     this.starting = false;
     this.stopping = false;
@@ -206,6 +209,16 @@ export class ChargingStation {
     this.idTagsCache = IdTagsCache.getInstance();
     this.chargingStationWorkerBroadcastChannel = new ChargingStationWorkerBroadcastChannel(this);
 
+    this.on(ChargingStationEvents.started, () => {
+      parentPort?.postMessage(buildStartedMessage(this));
+    });
+    this.on(ChargingStationEvents.stopped, () => {
+      parentPort?.postMessage(buildStoppedMessage(this));
+    });
+    this.on(ChargingStationEvents.updated, () => {
+      parentPort?.postMessage(buildUpdatedMessage(this));
+    });
+
     this.initialize();
   }
 
@@ -213,13 +226,42 @@ export class ChargingStation {
     return this.connectors.size === 0 && this.evses.size > 0;
   }
 
+  public get stationInfo(): ChargingStationInfo {
+    return {
+      ...{
+        enableStatistics: false,
+        remoteAuthorization: true,
+        currentOutType: CurrentType.AC,
+        mainVoltageMeterValues: true,
+        phaseLineToLineVoltageMeterValues: false,
+        customValueLimitationMeterValues: true,
+        ocppStrictCompliance: true,
+        outOfOrderEndMeterValues: false,
+        beginEndMeterValues: false,
+        meteringPerTransaction: true,
+        transactionDataMeterValues: false,
+        supervisionUrlOcppConfiguration: false,
+        supervisionUrlOcppKey: VendorParametersKey.ConnectionUrl,
+        ocppVersion: OCPPVersion.VERSION_16,
+        ocppPersistentConfiguration: true,
+        stationInfoPersistentConfiguration: true,
+        automaticTransactionGeneratorPersistentConfiguration: true,
+        autoReconnectMaxRetries: -1,
+        registrationMaxRetries: -1,
+        reconnectExponentialDelay: false,
+        stopTransactionsOnStopped: true,
+      },
+      ...this.internalStationInfo,
+    };
+  }
+
   private get wsConnectionUrl(): URL {
     return new URL(
       `${
-        this.getSupervisionUrlOcppConfiguration() &&
-        isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
-        isNotEmptyString(getConfigurationKey(this, this.getSupervisionUrlOcppKey())?.value)
-          ? getConfigurationKey(this, this.getSupervisionUrlOcppKey())!.value
+        this.stationInfo?.supervisionUrlOcppConfiguration &&
+        isNotEmptyString(this.stationInfo?.supervisionUrlOcppKey) &&
+        isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!)?.value)
+          ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!)!.value
           : this.configuredSupervisionUrl.href
       }/${this.stationInfo.chargingStationId}`,
     );
@@ -240,14 +282,6 @@ export class ChargingStation {
     return isNotEmptyArray(this.idTagsCache.getIdTags(getIdTagsFile(this.stationInfo)!));
   }
 
-  public getEnableStatistics(): boolean {
-    return this.stationInfo.enableStatistics ?? false;
-  }
-
-  public getRemoteAuthorization(): boolean {
-    return this.stationInfo.remoteAuthorization ?? true;
-  }
-
   public getNumberOfPhases(stationInfo?: ChargingStationInfo): number {
     const localStationInfo: ChargingStationInfo = stationInfo ?? this.stationInfo;
     switch (this.getCurrentOutType(stationInfo)) {
@@ -341,27 +375,6 @@ export class ChargingStation {
     return this.connectors.get(connectorId);
   }
 
-  public getCurrentOutType(stationInfo?: ChargingStationInfo): CurrentType {
-    return (stationInfo ?? this.stationInfo)?.currentOutType ?? CurrentType.AC;
-  }
-
-  public getOcppStrictCompliance(): boolean {
-    return this.stationInfo?.ocppStrictCompliance ?? true;
-  }
-
-  public getVoltageOut(stationInfo?: ChargingStationInfo): number {
-    const defaultVoltageOut = getDefaultVoltageOut(
-      this.getCurrentOutType(stationInfo),
-      this.logPrefix(),
-      this.templateFile,
-    );
-    return (stationInfo ?? this.stationInfo).voltageOut ?? defaultVoltageOut;
-  }
-
-  public getMaximumPower(stationInfo?: ChargingStationInfo): number {
-    return (stationInfo ?? this.stationInfo).maximumPower!;
-  }
-
   public getConnectorMaximumAvailablePower(connectorId: number): number {
     let connectorAmperageLimitationPowerLimit: number | undefined;
     if (
@@ -369,17 +382,17 @@ export class ChargingStation {
       this.getAmperageLimitation()! < this.stationInfo.maximumAmperage!
     ) {
       connectorAmperageLimitationPowerLimit =
-        (this.getCurrentOutType() === CurrentType.AC
+        (this.stationInfo?.currentOutType === CurrentType.AC
           ? ACElectricUtils.powerTotal(
               this.getNumberOfPhases(),
-              this.getVoltageOut(),
+              this.stationInfo.voltageOut!,
               this.getAmperageLimitation()! *
                 (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
             )
-          : DCElectricUtils.power(this.getVoltageOut(), this.getAmperageLimitation()!)) /
+          : DCElectricUtils.power(this.stationInfo.voltageOut!, this.getAmperageLimitation()!)) /
         this.powerDivider;
     }
-    const connectorMaximumPower = this.getMaximumPower() / this.powerDivider;
+    const connectorMaximumPower = this.stationInfo.maximumPower! / this.powerDivider;
     const connectorChargingProfilesPowerLimit =
       getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId);
     return min(
@@ -432,34 +445,6 @@ export class ChargingStation {
     return numberOfRunningTransactions;
   }
 
-  public getOutOfOrderEndMeterValues(): boolean {
-    return this.stationInfo?.outOfOrderEndMeterValues ?? false;
-  }
-
-  public getBeginEndMeterValues(): boolean {
-    return this.stationInfo?.beginEndMeterValues ?? false;
-  }
-
-  public getMeteringPerTransaction(): boolean {
-    return this.stationInfo?.meteringPerTransaction ?? true;
-  }
-
-  public getTransactionDataMeterValues(): boolean {
-    return this.stationInfo?.transactionDataMeterValues ?? false;
-  }
-
-  public getMainVoltageMeterValues(): boolean {
-    return this.stationInfo?.mainVoltageMeterValues ?? true;
-  }
-
-  public getPhaseLineToLineVoltageMeterValues(): boolean {
-    return this.stationInfo?.phaseLineToLineVoltageMeterValues ?? false;
-  }
-
-  public getCustomValueLimitationMeterValues(): boolean {
-    return this.stationInfo?.customValueLimitationMeterValues ?? true;
-  }
-
   public getConnectorIdByTransactionId(transactionId: number): number | undefined {
     if (this.hasEvses) {
       for (const evseStatus of this.evses.values()) {
@@ -528,10 +513,10 @@ export class ChargingStation {
 
   public setSupervisionUrl(url: string): void {
     if (
-      this.getSupervisionUrlOcppConfiguration() &&
-      isNotEmptyString(this.getSupervisionUrlOcppKey())
+      this.stationInfo?.supervisionUrlOcppConfiguration === true &&
+      isNotEmptyString(this.stationInfo?.supervisionUrlOcppKey)
     ) {
-      setConfigurationKeyValue(this, this.getSupervisionUrlOcppKey(), url);
+      setConfigurationKeyValue(this, this.stationInfo.supervisionUrlOcppKey!, url);
     } else {
       this.stationInfo.supervisionUrls = url;
       this.saveStationInfo();
@@ -656,7 +641,7 @@ export class ChargingStation {
     if (this.started === false) {
       if (this.starting === false) {
         this.starting = true;
-        if (this.getEnableStatistics() === true) {
+        if (this.stationInfo?.enableStatistics === true) {
           this.performanceStatistics?.start();
         }
         if (hasFeatureProfile(this, SupportedFeatureProfiles.Reservation)) {
@@ -664,57 +649,46 @@ export class ChargingStation {
         }
         this.openWSConnection();
         // Monitor charging station template file
-        // FIXME: Disabled until the spurious configuration file change detection is identified
-        // this.templateFileWatcher = watchJsonFile(
-        //   this.templateFile,
-        //   FileType.ChargingStationTemplate,
-        //   this.logPrefix(),
-        //   undefined,
-        //   (event, filename): void => {
-        //     if (isNotEmptyString(filename) && event === 'change') {
-        //       try {
-        //         logger.debug(
-        //           `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
-        //             this.templateFile
-        //           } file have changed, reload`,
-        //         );
-        //         this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash);
-        //         // Initialize
-        //         this.initialize();
-        //         this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo)!);
-        //         // Restart the ATG
-        //         this.stopAutomaticTransactionGenerator()
-        //           .then(() => {
-        //             delete this.automaticTransactionGeneratorConfiguration;
-        //             if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
-        //               this.startAutomaticTransactionGenerator();
-        //             }
-        //           })
-        //           .catch((err) =>
-        //             logger.error(
-        //               `${this.logPrefix()} failed to stop ATG at ${
-        //                 FileType.ChargingStationTemplate
-        //               } reload`,
-        //               err,
-        //             ),
-        //           );
-        //         if (this.getEnableStatistics() === true) {
-        //           this.performanceStatistics?.restart();
-        //         } else {
-        //           this.performanceStatistics?.stop();
-        //         }
-        //         // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
-        //       } catch (error) {
-        //         logger.error(
-        //           `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
-        //           error,
-        //         );
-        //       }
-        //     }
-        //   },
-        // );
+        this.templateFileWatcher = watchJsonFile(
+          this.templateFile,
+          FileType.ChargingStationTemplate,
+          this.logPrefix(),
+          undefined,
+          (event, filename): void => {
+            if (isNotEmptyString(filename) && event === 'change') {
+              try {
+                logger.debug(
+                  `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
+                    this.templateFile
+                  } file have changed, reload`,
+                );
+                this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash);
+                // Initialize
+                this.initialize();
+                this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo)!);
+                // Restart the ATG
+                this.stopAutomaticTransactionGenerator();
+                delete this.automaticTransactionGeneratorConfiguration;
+                if (this.getAutomaticTransactionGeneratorConfiguration().enable === true) {
+                  this.startAutomaticTransactionGenerator();
+                }
+                if (this.stationInfo?.enableStatistics === true) {
+                  this.performanceStatistics?.restart();
+                } else {
+                  this.performanceStatistics?.stop();
+                }
+                // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
+              } catch (error) {
+                logger.error(
+                  `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
+                  error,
+                );
+              }
+            }
+          },
+        );
         this.started = true;
-        parentPort?.postMessage(buildStartedMessage(this));
+        this.emit(ChargingStationEvents.started);
         this.starting = false;
       } else {
         logger.warn(`${this.logPrefix()} Charging station is already starting...`);
@@ -724,13 +698,13 @@ export class ChargingStation {
     }
   }
 
-  public async stop(reason?: StopTransactionReason): Promise<void> {
+  public async stop(reason?: StopTransactionReason, stopTransactions?: boolean): Promise<void> {
     if (this.started === true) {
       if (this.stopping === false) {
         this.stopping = true;
-        await this.stopMessageSequence(reason);
+        await this.stopMessageSequence(reason, stopTransactions);
         this.closeWSConnection();
-        if (this.getEnableStatistics() === true) {
+        if (this.stationInfo?.enableStatistics === true) {
           this.performanceStatistics?.stop();
         }
         if (hasFeatureProfile(this, SupportedFeatureProfiles.Reservation)) {
@@ -742,7 +716,7 @@ export class ChargingStation {
         delete this.bootNotificationResponse;
         this.started = false;
         this.saveConfiguration();
-        parentPort?.postMessage(buildStoppedMessage(this));
+        this.emit(ChargingStationEvents.stopped);
         this.stopping = false;
       } else {
         logger.warn(`${this.logPrefix()} Charging station is already stopping...`);
@@ -760,7 +734,7 @@ export class ChargingStation {
   }
 
   public saveOcppConfiguration(): void {
-    if (this.getOcppPersistentConfiguration()) {
+    if (this.stationInfo?.ocppPersistentConfiguration === true) {
       this.saveConfiguration();
     }
   }
@@ -808,7 +782,7 @@ export class ChargingStation {
 
     this.wsConnection = new WebSocket(
       this.wsConnectionUrl,
-      `ocpp${this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16}`,
+      `ocpp${this.stationInfo?.ocppVersion}`,
       options,
     );
 
@@ -847,17 +821,17 @@ export class ChargingStation {
       let automaticTransactionGeneratorConfiguration:
         | AutomaticTransactionGeneratorConfiguration
         | undefined;
-      const automaticTransactionGeneratorConfigurationFromFile =
-        this.getConfigurationFromFile()?.automaticTransactionGenerator;
+      const stationTemplate = this.getTemplateFromFile();
+      const stationConfiguration = this.getConfigurationFromFile();
       if (
-        this.getAutomaticTransactionGeneratorPersistentConfiguration() &&
-        automaticTransactionGeneratorConfigurationFromFile
+        this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true &&
+        stationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
+        stationConfiguration?.automaticTransactionGenerator
       ) {
         automaticTransactionGeneratorConfiguration =
-          automaticTransactionGeneratorConfigurationFromFile;
+          stationConfiguration?.automaticTransactionGenerator;
       } else {
-        automaticTransactionGeneratorConfiguration =
-          this.getTemplateFromFile()?.AutomaticTransactionGenerator;
+        automaticTransactionGeneratorConfiguration = stationTemplate?.AutomaticTransactionGenerator;
       }
       this.automaticTransactionGeneratorConfiguration = {
         ...Constants.DEFAULT_ATG_CONFIGURATION,
@@ -881,30 +855,30 @@ export class ChargingStation {
       this.automaticTransactionGenerator?.start();
     }
     this.saveAutomaticTransactionGeneratorConfiguration();
-    parentPort?.postMessage(buildUpdatedMessage(this));
+    this.emit(ChargingStationEvents.updated);
   }
 
-  public async stopAutomaticTransactionGenerator(connectorIds?: number[]): Promise<void> {
+  public stopAutomaticTransactionGenerator(connectorIds?: number[]): void {
     if (isNotEmptyArray(connectorIds)) {
       for (const connectorId of connectorIds!) {
-        await this.automaticTransactionGenerator?.stopConnector(connectorId);
+        this.automaticTransactionGenerator?.stopConnector(connectorId);
       }
     } else {
-      await this.automaticTransactionGenerator?.stop();
+      this.automaticTransactionGenerator?.stop();
     }
     this.saveAutomaticTransactionGeneratorConfiguration();
-    parentPort?.postMessage(buildUpdatedMessage(this));
+    this.emit(ChargingStationEvents.updated);
   }
 
   public async stopTransactionOnConnector(
     connectorId: number,
-    reason = StopTransactionReason.NONE,
+    reason?: StopTransactionReason,
   ): Promise<StopTransactionResponse> {
     const transactionId = this.getConnectorStatus(connectorId)?.transactionId;
     if (
-      this.getBeginEndMeterValues() === true &&
-      this.getOcppStrictCompliance() === true &&
-      this.getOutOfOrderEndMeterValues() === false
+      this.stationInfo?.beginEndMeterValues === true &&
+      this.stationInfo?.ocppStrictCompliance === true &&
+      this.stationInfo?.outOfOrderEndMeterValues === false
     ) {
       // FIXME: Implement OCPP version agnostic helpers
       const transactionEndMeterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(
@@ -928,7 +902,7 @@ export class ChargingStation {
       {
         transactionId,
         meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId!, true),
-        reason,
+        ...(isNullOrUndefined(reason) && { reason }),
       },
     );
   }
@@ -1101,14 +1075,6 @@ export class ChargingStation {
     }
   }
 
-  private getSupervisionUrlOcppConfiguration(): boolean {
-    return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
-  }
-
-  private getSupervisionUrlOcppKey(): string {
-    return this.stationInfo.supervisionUrlOcppKey ?? VendorParametersKey.ConnectionUrl;
-  }
-
   private getTemplateFromFile(): ChargingStationTemplate | undefined {
     let template: ChargingStationTemplate | undefined;
     try {
@@ -1149,6 +1115,7 @@ export class ChargingStation {
     stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate);
     stationInfo.ocppVersion = stationTemplate?.ocppVersion ?? OCPPVersion.VERSION_16;
     createSerialNumber(stationTemplate, stationInfo);
+    stationInfo.voltageOut = this.getVoltageOut(stationInfo);
     if (isNotEmptyArray(stationTemplate?.power)) {
       stationTemplate.power = stationTemplate.power as number[];
       const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length);
@@ -1163,6 +1130,7 @@ export class ChargingStation {
           ? stationTemplate.power * 1000
           : stationTemplate.power;
     }
+    stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo);
     stationInfo.firmwareVersionPattern =
       stationTemplate?.firmwareVersionPattern ?? Constants.SEMVER_PATTERN;
     if (
@@ -1187,13 +1155,12 @@ export class ChargingStation {
     stationInfo.resetTime = !isNullOrUndefined(stationTemplate?.resetTime)
       ? secondsToMilliseconds(stationTemplate.resetTime!)
       : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
-    stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo);
     return stationInfo;
   }
 
   private getStationInfoFromFile(): ChargingStationInfo | undefined {
     let stationInfo: ChargingStationInfo | undefined;
-    if (this.getStationInfoPersistentConfiguration()) {
+    if (this.stationInfo?.stationInfoPersistentConfiguration === true) {
       stationInfo = this.getConfigurationFromFile()?.stationInfo;
       if (stationInfo) {
         delete stationInfo?.infoHash;
@@ -1221,24 +1188,12 @@ export class ChargingStation {
   }
 
   private saveStationInfo(): void {
-    if (this.getStationInfoPersistentConfiguration()) {
+    if (this.stationInfo?.stationInfoPersistentConfiguration === true) {
       this.saveConfiguration();
     }
   }
 
-  private getOcppPersistentConfiguration(): boolean {
-    return this.stationInfo?.ocppPersistentConfiguration ?? true;
-  }
-
-  private getStationInfoPersistentConfiguration(): boolean {
-    return this.stationInfo?.stationInfoPersistentConfiguration ?? true;
-  }
-
-  private getAutomaticTransactionGeneratorPersistentConfiguration(): boolean {
-    return this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration ?? true;
-  }
-
-  private handleUnsupportedVersion(version: OCPPVersion) {
+  private handleUnsupportedVersion(version: OCPPVersion | undefined) {
     const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`;
     logger.error(`${this.logPrefix()} ${errorMsg}`);
     throw new BaseError(errorMsg);
@@ -1251,17 +1206,17 @@ export class ChargingStation {
       dirname(this.templateFile.replace('station-templates', 'configurations')),
       `${getHashId(this.index, stationTemplate)}.json`,
     );
-    const chargingStationConfiguration = this.getConfigurationFromFile();
+    const stationConfiguration = this.getConfigurationFromFile();
     if (
-      chargingStationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
+      stationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
       // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
-      (chargingStationConfiguration?.connectorsStatus || chargingStationConfiguration?.evsesStatus)
+      (stationConfiguration?.connectorsStatus || stationConfiguration?.evsesStatus)
     ) {
-      this.initializeConnectorsOrEvsesFromFile(chargingStationConfiguration);
+      this.initializeConnectorsOrEvsesFromFile(stationConfiguration);
     } else {
       this.initializeConnectorsOrEvsesFromTemplate(stationTemplate);
     }
-    this.stationInfo = this.getStationInfo();
+    this.internalStationInfo = this.getStationInfo();
     if (
       this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
       isNotEmptyString(this.stationInfo.firmwareVersion) &&
@@ -1282,7 +1237,7 @@ export class ChargingStation {
     }
     this.saveStationInfo();
     this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
-    if (this.getEnableStatistics() === true) {
+    if (this.stationInfo?.enableStatistics === true) {
       this.performanceStatistics = PerformanceStatistics.getInstance(
         this.stationInfo.hashId,
         this.stationInfo.chargingStationId!,
@@ -1295,6 +1250,11 @@ export class ChargingStation {
     this.ocppConfiguration = this.getOcppConfiguration();
     this.initializeOcppConfiguration();
     this.initializeOcppServices();
+    this.once(ChargingStationEvents.accepted, () => {
+      this.startMessageSequence().catch((error) => {
+        logger.error(`${this.logPrefix()} Error while starting the message sequence:`, error);
+      });
+    });
     if (this.stationInfo?.autoRegister === true) {
       this.bootNotificationResponse = {
         currentTime: new Date(),
@@ -1305,7 +1265,7 @@ export class ChargingStation {
   }
 
   private initializeOcppServices(): void {
-    const ocppVersion = this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
+    const ocppVersion = this.stationInfo?.ocppVersion;
     switch (ocppVersion) {
       case OCPPVersion.VERSION_16:
         this.ocppIncomingRequestService =
@@ -1336,22 +1296,22 @@ export class ChargingStation {
       addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', { visible: false });
     }
     if (
-      this.getSupervisionUrlOcppConfiguration() &&
-      isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
-      !getConfigurationKey(this, this.getSupervisionUrlOcppKey())
+      this.stationInfo?.supervisionUrlOcppConfiguration &&
+      isNotEmptyString(this.stationInfo?.supervisionUrlOcppKey) &&
+      !getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!)
     ) {
       addConfigurationKey(
         this,
-        this.getSupervisionUrlOcppKey(),
+        this.stationInfo.supervisionUrlOcppKey!,
         this.configuredSupervisionUrl.href,
         { reboot: true },
       );
     } else if (
-      !this.getSupervisionUrlOcppConfiguration() &&
-      isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
-      getConfigurationKey(this, this.getSupervisionUrlOcppKey())
+      !this.stationInfo?.supervisionUrlOcppConfiguration &&
+      isNotEmptyString(this.stationInfo?.supervisionUrlOcppKey) &&
+      getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!)
     ) {
-      deleteConfigurationKey(this, this.getSupervisionUrlOcppKey(), { save: false });
+      deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!, { save: false });
     }
     if (
       isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
@@ -1642,7 +1602,7 @@ export class ChargingStation {
   }
 
   private saveAutomaticTransactionGeneratorConfiguration(): void {
-    if (this.getAutomaticTransactionGeneratorPersistentConfiguration()) {
+    if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true) {
       this.saveConfiguration();
     }
   }
@@ -1664,12 +1624,15 @@ export class ChargingStation {
         let configurationData: ChargingStationConfiguration = this.getConfigurationFromFile()
           ? cloneObject<ChargingStationConfiguration>(this.getConfigurationFromFile()!)
           : {};
-        if (this.getStationInfoPersistentConfiguration() && this.stationInfo) {
+        if (this.stationInfo?.stationInfoPersistentConfiguration === true && this.stationInfo) {
           configurationData.stationInfo = this.stationInfo;
         } else {
           delete configurationData.stationInfo;
         }
-        if (this.getOcppPersistentConfiguration() && this.ocppConfiguration?.configurationKey) {
+        if (
+          this.stationInfo?.ocppPersistentConfiguration === true &&
+          this.ocppConfiguration?.configurationKey
+        ) {
           configurationData.configurationKey = this.ocppConfiguration.configurationKey;
         } else {
           delete configurationData.configurationKey;
@@ -1679,7 +1642,7 @@ export class ChargingStation {
           buildChargingStationAutomaticTransactionGeneratorConfiguration(this),
         );
         if (
-          !this.getAutomaticTransactionGeneratorPersistentConfiguration() ||
+          !this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration ||
           !this.getAutomaticTransactionGeneratorConfiguration()
         ) {
           delete configurationData.automaticTransactionGenerator;
@@ -1701,6 +1664,10 @@ export class ChargingStation {
               stationInfo: configurationData.stationInfo,
               configurationKey: configurationData.configurationKey,
               automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
+              ...(this.connectors.size > 0 && {
+                connectorsStatus: configurationData.connectorsStatus,
+              }),
+              ...(this.evses.size > 0 && { evsesStatus: configurationData.evsesStatus }),
             } as ChargingStationConfiguration),
           )
           .digest('hex');
@@ -1754,7 +1721,7 @@ export class ChargingStation {
 
   private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
     const configurationKey = this.getConfigurationFromFile()?.configurationKey;
-    if (this.getOcppPersistentConfiguration() === true && configurationKey) {
+    if (this.stationInfo?.ocppPersistentConfiguration === true && configurationKey) {
       return { configurationKey };
     }
     return undefined;
@@ -1774,9 +1741,9 @@ export class ChargingStation {
       logger.info(
         `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`,
       );
+      let registrationRetryCount = 0;
       if (this.isRegistered() === false) {
         // Send BootNotification
-        let registrationRetryCount = 0;
         do {
           this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
             BootNotificationRequest,
@@ -1785,7 +1752,7 @@ export class ChargingStation {
             skipBufferingOnError: true,
           });
           if (this.isRegistered() === false) {
-            this.getRegistrationMaxRetries() !== -1 && ++registrationRetryCount;
+            this.stationInfo?.registrationMaxRetries !== -1 && ++registrationRetryCount;
             await sleep(
               this?.bootNotificationResponse?.interval
                 ? secondsToMilliseconds(this.bootNotificationResponse.interval)
@@ -1794,22 +1761,24 @@ export class ChargingStation {
           }
         } while (
           this.isRegistered() === false &&
-          (registrationRetryCount <= this.getRegistrationMaxRetries()! ||
-            this.getRegistrationMaxRetries() === -1)
+          (registrationRetryCount <= this.stationInfo.registrationMaxRetries! ||
+            this.stationInfo?.registrationMaxRetries === -1)
         );
       }
       if (this.isRegistered() === true) {
+        this.emit(ChargingStationEvents.registered);
         if (this.inAcceptedState() === true) {
-          await this.startMessageSequence();
+          this.emit(ChargingStationEvents.accepted);
         }
       } else {
         logger.error(
-          `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`,
+          `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${this
+            .stationInfo?.registrationMaxRetries})`,
         );
       }
       this.wsConnectionRestarted = false;
       this.autoReconnectRetryCount = 0;
-      parentPort?.postMessage(buildUpdatedMessage(this));
+      this.emit(ChargingStationEvents.updated);
     } else {
       logger.warn(
         `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`,
@@ -1839,7 +1808,7 @@ export class ChargingStation {
         this.started === true && (await this.reconnect());
         break;
     }
-    parentPort?.postMessage(buildUpdatedMessage(this));
+    this.emit(ChargingStationEvents.updated);
   }
 
   private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
@@ -1859,7 +1828,7 @@ export class ChargingStation {
 
   private async handleIncomingMessage(request: IncomingRequest): Promise<void> {
     const [messageType, messageId, commandName, commandPayload] = request;
-    if (this.getEnableStatistics() === true) {
+    if (this.stationInfo?.enableStatistics === true) {
       this.performanceStatistics?.addRequestStatistic(commandName, messageType);
     }
     logger.debug(
@@ -1950,7 +1919,7 @@ export class ChargingStation {
             logger.error(`${this.logPrefix()} ${errorMsg}`);
             throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg);
         }
-        parentPort?.postMessage(buildUpdatedMessage(this));
+        this.emit(ChargingStationEvents.updated);
       } else {
         throw new OCPPError(
           ErrorType.PROTOCOL_ERROR,
@@ -2021,7 +1990,7 @@ export class ChargingStation {
   }
 
   private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
-    if (this.getMeteringPerTransaction() === true) {
+    if (this.stationInfo?.meteringPerTransaction === true) {
       return (
         (rounded === true
           ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue!)
@@ -2039,7 +2008,7 @@ export class ChargingStation {
     return stationTemplate?.useConnectorId0 ?? true;
   }
 
-  private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
+  private async stopRunningTransactions(reason?: StopTransactionReason): Promise<void> {
     if (this.hasEvses) {
       for (const [evseId, evseStatus] of this.evses) {
         if (evseId === 0) {
@@ -2071,19 +2040,9 @@ export class ChargingStation {
     return Constants.DEFAULT_CONNECTION_TIMEOUT;
   }
 
-  // -1 for unlimited, 0 for disabling
-  private getAutoReconnectMaxRetries(): number | undefined {
-    return this.stationInfo.autoReconnectMaxRetries ?? -1;
-  }
-
-  // -1 for unlimited, 0 for disabling
-  private getRegistrationMaxRetries(): number | undefined {
-    return this.stationInfo.registrationMaxRetries ?? -1;
-  }
-
   private getPowerDivider(): number {
     let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors();
-    if (this.stationInfo?.powerSharedByConnectors) {
+    if (this.stationInfo?.powerSharedByConnectors === true) {
       powerDivider = this.getNumberOfRunningTransactions();
     }
     return powerDivider;
@@ -2103,6 +2062,23 @@ export class ChargingStation {
     }
   }
 
+  private getMaximumPower(stationInfo?: ChargingStationInfo): number {
+    return (stationInfo ?? this.stationInfo).maximumPower!;
+  }
+
+  private getCurrentOutType(stationInfo?: ChargingStationInfo): CurrentType {
+    return (stationInfo ?? this.stationInfo).currentOutType ?? CurrentType.AC;
+  }
+
+  private getVoltageOut(stationInfo?: ChargingStationInfo): number {
+    const defaultVoltageOut = getDefaultVoltageOut(
+      this.getCurrentOutType(stationInfo),
+      this.logPrefix(),
+      this.templateFile,
+    );
+    return (stationInfo ?? this.stationInfo).voltageOut ?? defaultVoltageOut;
+  }
+
   private getAmperageLimitation(): number | undefined {
     if (
       isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
@@ -2167,25 +2143,26 @@ export class ChargingStation {
     }
 
     // Start the ATG
-    if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
+    if (this.getAutomaticTransactionGeneratorConfiguration().enable === true) {
       this.startAutomaticTransactionGenerator();
     }
     this.wsConnectionRestarted === true && this.flushMessageBuffer();
   }
 
   private async stopMessageSequence(
-    reason: StopTransactionReason = StopTransactionReason.NONE,
+    reason?: StopTransactionReason,
+    stopTransactions = this.stationInfo?.stopTransactionsOnStopped,
   ): Promise<void> {
     // Stop WebSocket ping
     this.stopWebSocketPing();
     // Stop heartbeat
     this.stopHeartbeat();
-    // Stop ongoing transactions
+    // Stop the ATG
     if (this.automaticTransactionGenerator?.started === true) {
-      await this.stopAutomaticTransactionGenerator();
-    } else {
-      await this.stopRunningTransactions(reason);
+      this.stopAutomaticTransactionGenerator();
     }
+    // Stop ongoing transactions
+    stopTransactions && (await this.stopRunningTransactions(reason));
     if (this.hasEvses) {
       for (const [evseId, evseStatus] of this.evses) {
         if (evseId > 0) {
@@ -2318,10 +2295,6 @@ export class ChargingStation {
     }
   }
 
-  private getReconnectExponentialDelay(): boolean {
-    return this.stationInfo?.reconnectExponentialDelay ?? false;
-  }
-
   private async reconnect(): Promise<void> {
     // Stop WebSocket ping
     this.stopWebSocketPing();
@@ -2329,16 +2302,17 @@ export class ChargingStation {
     this.stopHeartbeat();
     // Stop the ATG if needed
     if (this.getAutomaticTransactionGeneratorConfiguration().stopOnConnectionFailure === true) {
-      await this.stopAutomaticTransactionGenerator();
+      this.stopAutomaticTransactionGenerator();
     }
     if (
-      this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries()! ||
-      this.getAutoReconnectMaxRetries() === -1
+      this.autoReconnectRetryCount < this.stationInfo.autoReconnectMaxRetries! ||
+      this.stationInfo?.autoReconnectMaxRetries === -1
     ) {
       ++this.autoReconnectRetryCount;
-      const reconnectDelay = this.getReconnectExponentialDelay()
-        ? exponentialDelay(this.autoReconnectRetryCount)
-        : secondsToMilliseconds(this.getConnectionTimeout());
+      const reconnectDelay =
+        this.stationInfo?.reconnectExponentialDelay === true
+          ? exponentialDelay(this.autoReconnectRetryCount)
+          : secondsToMilliseconds(this.getConnectionTimeout());
       const reconnectDelayWithdraw = 1000;
       const reconnectTimeout =
         reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
@@ -2361,11 +2335,11 @@ export class ChargingStation {
         { closeOpened: true },
       );
       this.wsConnectionRestarted = true;
-    } else if (this.getAutoReconnectMaxRetries() !== -1) {
+    } else if (this.stationInfo?.autoReconnectMaxRetries !== -1) {
       logger.error(
         `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
           this.autoReconnectRetryCount
-        }) or retries disabled (${this.getAutoReconnectMaxRetries()})`,
+        }) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries})`,
       );
     }
   }