fix: disable dynamic reload until spurious file change is identified
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
index d08079c805760799b7e47d0f9c82dafc8bd7935a..bd3fb81283b079f66ceed55b380434666c331613 100644 (file)
@@ -1,15 +1,7 @@
 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
 
 import { createHash } from 'node:crypto';
-import {
-  type FSWatcher,
-  closeSync,
-  existsSync,
-  mkdirSync,
-  openSync,
-  readFileSync,
-  writeFileSync,
-} from 'node:fs';
+import { type FSWatcher, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
 import { dirname, join } from 'node:path';
 import { URL } from 'node:url';
 import { parentPort } from 'node:worker_threads';
@@ -28,6 +20,7 @@ import {
 } from './ConfigurationKeyUtils';
 import {
   buildConnectorsMap,
+  checkChargingStation,
   checkConnectorsConfiguration,
   checkStationInfoConnectorStatus,
   checkTemplate,
@@ -151,10 +144,12 @@ import {
   isUndefined,
   logPrefix,
   logger,
+  min,
+  once,
   roundTo,
   secureRandom,
   sleep,
-  watchJsonFile,
+  // watchJsonFile,
 } from '../utils';
 
 export class ChargingStation {
@@ -166,7 +161,7 @@ export class ChargingStation {
   public idTagsCache: IdTagsCache;
   public automaticTransactionGenerator!: AutomaticTransactionGenerator | undefined;
   public ocppConfiguration!: ChargingStationOcppConfiguration | undefined;
-  public wsConnection!: WebSocket | null;
+  public wsConnection: WebSocket | null;
   public readonly connectors: Map<number, ConnectorStatus>;
   public readonly evses: Map<number, EvseStatus>;
   public readonly requests: Map<string, CachedRequest>;
@@ -198,6 +193,7 @@ export class ChargingStation {
     this.started = false;
     this.starting = false;
     this.stopping = false;
+    this.wsConnection = null;
     this.wsConnectionRestarted = false;
     this.autoReconnectRetryCount = 0;
     this.index = index;
@@ -363,12 +359,7 @@ export class ChargingStation {
   }
 
   public getMaximumPower(stationInfo?: ChargingStationInfo): number {
-    const localStationInfo = stationInfo ?? this.stationInfo;
-    // eslint-disable-next-line @typescript-eslint/dot-notation
-    return (
-      (localStationInfo['maxPower' as keyof ChargingStationInfo] as number) ??
-      localStationInfo.maximumPower
-    );
+    return (stationInfo ?? this.stationInfo).maximumPower!;
   }
 
   public getConnectorMaximumAvailablePower(connectorId: number): number {
@@ -391,7 +382,7 @@ export class ChargingStation {
     const connectorMaximumPower = this.getMaximumPower() / this.powerDivider;
     const connectorChargingProfilesPowerLimit =
       getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId);
-    return Math.min(
+    return min(
       isNaN(connectorMaximumPower) ? Infinity : connectorMaximumPower,
       isNaN(connectorAmperageLimitationPowerLimit!)
         ? Infinity
@@ -573,8 +564,7 @@ export class ChargingStation {
       );
     } else {
       logger.error(
-        `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()},
-          not starting the heartbeat`,
+        `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()}, not starting the heartbeat`,
       );
     }
   }
@@ -609,8 +599,7 @@ export class ChargingStation {
     }
     if (this.getConnectorStatus(connectorId)?.transactionStarted === false) {
       logger.error(
-        `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}
-          with no transaction started`,
+        `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started`,
       );
       return;
     } else if (
@@ -618,8 +607,7 @@ export class ChargingStation {
       isNullOrUndefined(this.getConnectorStatus(connectorId)?.transactionId)
     ) {
       logger.error(
-        `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}
-          with no transaction id`,
+        `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id`,
       );
       return;
     }
@@ -676,44 +664,55 @@ export class ChargingStation {
         }
         this.openWSConnection();
         // Monitor charging station template file
-        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.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,
-                );
-              }
-            }
-          },
-        );
+        // 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.started = true;
         parentPort?.postMessage(buildStartedMessage(this));
         this.starting = false;
@@ -771,19 +770,16 @@ export class ChargingStation {
   }
 
   public openWSConnection(
-    options: WsOptions = this.stationInfo?.wsOptions ?? {},
-    params: { closeOpened?: boolean; terminateOpened?: boolean } = {
-      closeOpened: false,
-      terminateOpened: false,
-    },
+    options?: WsOptions,
+    params?: { closeOpened?: boolean; terminateOpened?: boolean },
   ): void {
-    options = { handshakeTimeout: secondsToMilliseconds(this.getConnectionTimeout()), ...options };
+    options = {
+      handshakeTimeout: secondsToMilliseconds(this.getConnectionTimeout()),
+      ...this.stationInfo?.wsOptions,
+      ...options,
+    };
     params = { ...{ closeOpened: false, terminateOpened: false }, ...params };
-    if (this.started === false && this.starting === false) {
-      logger.warn(
-        `${this.logPrefix()} Cannot open OCPP connection to URL ${this.wsConnectionUrl.toString()}
-          on stopped charging station`,
-      );
+    if (!checkChargingStation(this, this.logPrefix())) {
       return;
     }
     if (
@@ -801,8 +797,7 @@ export class ChargingStation {
 
     if (this.isWebSocketConnectionOpened() === true) {
       logger.warn(
-        `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()}
-          is already opened`,
+        `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} is already opened`,
       );
       return;
     }
@@ -889,13 +884,13 @@ export class ChargingStation {
     parentPort?.postMessage(buildUpdatedMessage(this));
   }
 
-  public stopAutomaticTransactionGenerator(connectorIds?: number[]): void {
+  public async stopAutomaticTransactionGenerator(connectorIds?: number[]): Promise<void> {
     if (isNotEmptyArray(connectorIds)) {
       for (const connectorId of connectorIds!) {
-        this.automaticTransactionGenerator?.stopConnector(connectorId);
+        await this.automaticTransactionGenerator?.stopConnector(connectorId);
       }
     } else {
-      this.automaticTransactionGenerator?.stop();
+      await this.automaticTransactionGenerator?.stop();
     }
     this.saveAutomaticTransactionGeneratorConfiguration();
     parentPort?.postMessage(buildUpdatedMessage(this));
@@ -1036,8 +1031,7 @@ export class ChargingStation {
   }
 
   private startReservationExpirationSetInterval(customInterval?: number): void {
-    const interval =
-      customInterval ?? Constants.DEFAULT_RESERVATION_EXPIRATION_OBSERVATION_INTERVAL;
+    const interval = customInterval ?? Constants.DEFAULT_RESERVATION_EXPIRATION_INTERVAL;
     if (interval > 0) {
       logger.info(
         `${this.logPrefix()} Reservation expiration date checks started every ${formatDurationMilliSeconds(
@@ -1051,7 +1045,7 @@ export class ChargingStation {
   }
 
   private stopReservationExpirationSetInterval(): void {
-    if (this.reservationExpirationSetInterval) {
+    if (!isNullOrUndefined(this.reservationExpirationSetInterval)) {
       clearInterval(this.reservationExpirationSetInterval);
     }
   }
@@ -1145,7 +1139,8 @@ export class ChargingStation {
   private getStationInfoFromTemplate(): ChargingStationInfo {
     const stationTemplate: ChargingStationTemplate = this.getTemplateFromFile()!;
     checkTemplate(stationTemplate, this.logPrefix(), this.templateFile);
-    warnTemplateKeysDeprecation(stationTemplate, this.logPrefix(), this.templateFile);
+    const warnTemplateKeysDeprecationOnce = once(warnTemplateKeysDeprecation, this);
+    warnTemplateKeysDeprecationOnce(stationTemplate, this.logPrefix(), this.templateFile);
     if (stationTemplate?.Connectors) {
       checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile);
     }
@@ -1244,8 +1239,7 @@ export class ChargingStation {
   }
 
   private handleUnsupportedVersion(version: OCPPVersion) {
-    const errorMsg = `Unsupported protocol version '${version}' configured
-      in template file ${this.templateFile}`;
+    const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`;
     logger.error(`${this.logPrefix()} ${errorMsg}`);
     throw new BaseError(errorMsg);
   }
@@ -1512,7 +1506,7 @@ export class ChargingStation {
           for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
             if (
               connectorId === 0 &&
-              (!stationTemplate?.Connectors[connectorId] ||
+              (!stationTemplate?.Connectors?.[connectorId] ||
                 this.getUseConnectorId0(stationTemplate) === false)
             ) {
               continue;
@@ -1569,6 +1563,13 @@ export class ChargingStation {
         } with evse id 0 with no connector id 0 configuration`,
       );
     }
+    if (Object.keys(stationTemplate?.Evses?.[0]?.Connectors as object).length > 1) {
+      logger.warn(
+        `${this.logPrefix()} Charging station information from template ${
+          this.templateFile
+        } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`,
+      );
+    }
     if (stationTemplate?.Evses) {
       const evsesConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
         .update(JSON.stringify(stationTemplate?.Evses))
@@ -1580,11 +1581,11 @@ export class ChargingStation {
         this.evsesConfigurationHash = evsesConfigHash;
         const templateMaxEvses = getMaxNumberOfEvses(stationTemplate?.Evses);
         if (templateMaxEvses > 0) {
-          for (const evse in stationTemplate.Evses) {
-            const evseId = convertToInt(evse);
+          for (const evseKey in stationTemplate.Evses) {
+            const evseId = convertToInt(evseKey);
             this.evses.set(evseId, {
               connectors: buildConnectorsMap(
-                stationTemplate?.Evses[evse]?.Connectors,
+                stationTemplate?.Evses[evseKey]?.Connectors,
                 this.logPrefix(),
                 this.templateFile,
               ),
@@ -1704,30 +1705,27 @@ export class ChargingStation {
           )
           .digest('hex');
         if (this.configurationFileHash !== configurationHash) {
-          AsyncLock.acquire(AsyncLockType.configuration)
-            .then(() => {
-              configurationData.configurationHash = configurationHash;
-              const measureId = `${FileType.ChargingStationConfiguration} write`;
-              const beginId = PerformanceStatistics.beginMeasure(measureId);
-              const fileDescriptor = openSync(this.configurationFile, 'w');
-              writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
-              closeSync(fileDescriptor);
-              PerformanceStatistics.endMeasure(measureId, beginId);
-              this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
-              this.sharedLRUCache.setChargingStationConfiguration(configurationData);
-              this.configurationFileHash = configurationHash;
-            })
-            .catch((error) => {
-              handleFileException(
-                this.configurationFile,
-                FileType.ChargingStationConfiguration,
-                error as NodeJS.ErrnoException,
-                this.logPrefix(),
-              );
-            })
-            .finally(() => {
-              AsyncLock.release(AsyncLockType.configuration).catch(Constants.EMPTY_FUNCTION);
-            });
+          AsyncLock.runExclusive(AsyncLockType.configuration, () => {
+            configurationData.configurationHash = configurationHash;
+            const measureId = `${FileType.ChargingStationConfiguration} write`;
+            const beginId = PerformanceStatistics.beginMeasure(measureId);
+            writeFileSync(
+              this.configurationFile,
+              JSON.stringify(configurationData, undefined, 2),
+              'utf8',
+            );
+            PerformanceStatistics.endMeasure(measureId, beginId);
+            this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
+            this.sharedLRUCache.setChargingStationConfiguration(configurationData);
+            this.configurationFileHash = configurationHash;
+          }).catch((error) => {
+            handleFileException(
+              this.configurationFile,
+              FileType.ChargingStationConfiguration,
+              error as NodeJS.ErrnoException,
+              this.logPrefix(),
+            );
+          });
         } else {
           logger.debug(
             `${this.logPrefix()} Not saving unchanged charging station configuration file ${
@@ -2075,12 +2073,10 @@ export class ChargingStation {
 
   // -1 for unlimited, 0 for disabling
   private getAutoReconnectMaxRetries(): number | undefined {
-    return (
-      this.stationInfo.autoReconnectMaxRetries ?? Configuration.getAutoReconnectMaxRetries() ?? -1
-    );
+    return this.stationInfo.autoReconnectMaxRetries ?? -1;
   }
 
-  // 0 for disabling
+  // -1 for unlimited, 0 for disabling
   private getRegistrationMaxRetries(): number | undefined {
     return this.stationInfo.registrationMaxRetries ?? -1;
   }
@@ -2186,7 +2182,7 @@ export class ChargingStation {
     this.stopHeartbeat();
     // Stop ongoing transactions
     if (this.automaticTransactionGenerator?.started === true) {
-      this.stopAutomaticTransactionGenerator();
+      await this.stopAutomaticTransactionGenerator();
     } else {
       await this.stopRunningTransactions(reason);
     }
@@ -2333,7 +2329,7 @@ export class ChargingStation {
     this.stopHeartbeat();
     // Stop the ATG if needed
     if (this.getAutomaticTransactionGeneratorConfiguration().stopOnConnectionFailure === true) {
-      this.stopAutomaticTransactionGenerator();
+      await this.stopAutomaticTransactionGenerator();
     }
     if (
       this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries()! ||
@@ -2360,7 +2356,6 @@ export class ChargingStation {
       );
       this.openWSConnection(
         {
-          ...(this.stationInfo?.wsOptions ?? {}),
           handshakeTimeout: reconnectTimeout,
         },
         { closeOpened: true },