feat: add template tunable to stop transaction at CS stop
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
index 441c5d1e95652a8d7cfc67174cf1b2834848cea5..033152d6bf0d62c760f4477f3324351198c4f31b 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,10 +151,10 @@ 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;
@@ -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();
   }
 
@@ -664,57 +677,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.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.emit(ChargingStationEvents.started);
         this.starting = false;
       } else {
         logger.warn(`${this.logPrefix()} Charging station is already starting...`);
@@ -742,7 +744,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...`);
@@ -847,17 +849,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
+        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,7 +883,7 @@ export class ChargingStation {
       this.automaticTransactionGenerator?.start();
     }
     this.saveAutomaticTransactionGeneratorConfiguration();
-    parentPort?.postMessage(buildUpdatedMessage(this));
+    this.emit(ChargingStationEvents.updated);
   }
 
   public stopAutomaticTransactionGenerator(connectorIds?: number[]): void {
@@ -893,7 +895,7 @@ export class ChargingStation {
       this.automaticTransactionGenerator?.stop();
     }
     this.saveAutomaticTransactionGeneratorConfiguration();
-    parentPort?.postMessage(buildUpdatedMessage(this));
+    this.emit(ChargingStationEvents.updated);
   }
 
   public async stopTransactionOnConnector(
@@ -1251,13 +1253,13 @@ 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);
     }
@@ -1803,7 +1805,9 @@ export class ChargingStation {
         );
       }
       if (this.isRegistered() === true) {
+        this.emit(ChargingStationEvents.registered);
         if (this.inAcceptedState() === true) {
+          this.emit(ChargingStationEvents.accepted);
           await this.startMessageSequence();
         }
       } else {
@@ -1813,7 +1817,7 @@ export class ChargingStation {
       }
       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`,
@@ -1843,7 +1847,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 {
@@ -1954,7 +1958,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,
@@ -2171,7 +2175,7 @@ export class ChargingStation {
     }
 
     // Start the ATG
-    if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
+    if (this.getAutomaticTransactionGeneratorConfiguration().enable === true) {
       this.startAutomaticTransactionGenerator();
     }
     this.wsConnectionRestarted === true && this.flushMessageBuffer();
@@ -2179,7 +2183,7 @@ export class ChargingStation {
 
   private async stopMessageSequence(
     reason?: StopTransactionReason,
-    stopTransactions = true,
+    stopTransactions = this.stationInfo?.stopTransactionsOnStopped ?? true,
   ): Promise<void> {
     // Stop WebSocket ping
     this.stopWebSocketPing();