ATG: add support for idTag distribution algorithms
authorJérôme Benoit <jerome.benoit@sap.com>
Sun, 25 Sep 2022 20:57:31 +0000 (22:57 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Sun, 25 Sep 2022 20:57:31 +0000 (22:57 +0200)
Close #179

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
README.md
src/charging-station/AutomaticTransactionGenerator.ts
src/charging-station/Bootstrap.ts
src/charging-station/ChargingStation.ts
src/charging-station/ChargingStationUtils.ts
src/charging-station/ChargingStationWorker.ts
src/charging-station/MessageChannelUtils.ts
src/types/AutomaticTransactionGenerator.ts
src/types/ChargingStationWorker.ts
src/types/ConfigurationData.ts
src/types/Worker.ts

index 927aaffd4ec10ddd8aa699e34a33d5eb6a924ab8..b21f6a3beee2c0a3ff6b45a679e26a7eda775f46 100644 (file)
--- a/README.md
+++ b/README.md
@@ -91,7 +91,7 @@ But the modifications to test have to be done to the files in the build target d
 | Key                        | Value(s)                                         | Default Value                                                                                                                                                                                                  | Value type                                                                                                                                                                                                                          | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
 | -------------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
 | supervisionUrls            |                                                  | []                                                                                                                                                                                                             | string \| string[]                                                                                                                                                                                                                  | string or array of global connection URIs to OCPP-J servers                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
-| supervisionUrlDistribution | round-robin/random/sequential                    | round-robin                                                                                                                                                                                                    | boolean                                                                                                                                                                                                                             | supervision urls distribution policy to simulated charging stations                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
+| supervisionUrlDistribution | round-robin/random/charging-station-affinity     | charging-station-affinity                                                                                                                                                                                      | boolean                                                                                                                                                                                                                             | supervision urls distribution policy to simulated charging stations                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
 | logStatisticsInterval      |                                                  | 60                                                                                                                                                                                                             | integer                                                                                                                                                                                                                             | seconds between charging stations statistics output in the logs                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
 | logConsole                 | true/false                                       | false                                                                                                                                                                                                          | boolean                                                                                                                                                                                                                             | output logs on the console                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
 | logFormat                  |                                                  | simple                                                                                                                                                                                                         | string                                                                                                                                                                                                                              | winston log format                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
@@ -201,6 +201,25 @@ But the modifications to test have to be done to the files in the build target d
 
 #### AutomaticTransactionGenerator section
 
+Section type definition:
+
+```ts
+type AutomaticTransactionGeneratorConfiguration = {
+  enable: boolean;
+  minDuration: number;
+  maxDuration: number;
+  minDelayBetweenTwoTransactions: number;
+  maxDelayBetweenTwoTransactions: number;
+  probabilityOfStart: number;
+  stopAfterHours: number;
+  stopOnConnectionFailure: boolean;
+  requireAuthorize?: boolean;
+  idTagDistribution?: 'random' | 'round-robin' | 'connector-affinity';
+};
+```
+
+Section example:
+
 ```json
   "AutomaticTransactionGenerator": {
     "enable": false,
@@ -211,7 +230,8 @@ But the modifications to test have to be done to the files in the build target d
     "probabilityOfStart": 1,
     "stopAfterHours": 0.3,
     "stopOnConnectionFailure": true,
-    "requireAuthorize": true
+    "requireAuthorize": true,
+    "idTagDistribution": "random"
   }
 ```
 
index 3afb86d415c6a91fce7b19dd9979a37b80c1be2d..9b196ed9d15bdf6080f5eb0fb78991740dd79226 100644 (file)
@@ -2,9 +2,10 @@
 
 import BaseError from '../exception/BaseError';
 import PerformanceStatistics from '../performance/PerformanceStatistics';
-import type {
-  AutomaticTransactionGeneratorConfiguration,
-  Status,
+import {
+  type AutomaticTransactionGeneratorConfiguration,
+  IdTagDistribution,
+  type Status,
 } from '../types/AutomaticTransactionGenerator';
 import { RequestCommand } from '../types/ocpp/Requests';
 import {
@@ -20,6 +21,7 @@ import Constants from '../utils/Constants';
 import logger from '../utils/Logger';
 import Utils from '../utils/Utils';
 import type ChargingStation from './ChargingStation';
+import { ChargingStationUtils } from './ChargingStationUtils';
 
 export default class AutomaticTransactionGenerator {
   private static readonly instances: Map<string, AutomaticTransactionGenerator> = new Map<
@@ -31,6 +33,7 @@ export default class AutomaticTransactionGenerator {
   public readonly configuration: AutomaticTransactionGeneratorConfiguration;
   public started: boolean;
   private readonly chargingStation: ChargingStation;
+  private idTagIndex: number;
 
   private constructor(
     automaticTransactionGeneratorConfiguration: AutomaticTransactionGeneratorConfiguration,
@@ -39,6 +42,7 @@ export default class AutomaticTransactionGenerator {
     this.started = false;
     this.configuration = automaticTransactionGeneratorConfiguration;
     this.chargingStation = chargingStation;
+    this.idTagIndex = 0;
     this.connectorsStatus = new Map<number, Status>();
     this.initializeConnectorsStatus();
   }
@@ -313,7 +317,7 @@ export default class AutomaticTransactionGenerator {
     const beginId = PerformanceStatistics.beginMeasure(measureId);
     let startResponse: StartTransactionResponse;
     if (this.chargingStation.hasAuthorizedTags()) {
-      const idTag = this.chargingStation.getRandomIdTag();
+      const idTag = this.getIdTag(connectorId);
       const startTransactionLogMsg = `${this.logPrefix(
         connectorId
       )} start transaction with an idTag '${idTag}'`;
@@ -401,6 +405,41 @@ export default class AutomaticTransactionGenerator {
     return this.configuration?.requireAuthorize ?? true;
   }
 
+  private getRandomIdTag(authorizationFile: string): string {
+    const tags = this.chargingStation.authorizedTagsCache.getAuthorizedTags(authorizationFile);
+    this.idTagIndex = Math.floor(Utils.secureRandom() * tags.length);
+    return tags[this.idTagIndex];
+  }
+
+  private getRoundRobinIdTag(authorizationFile: string): string {
+    const tags = this.chargingStation.authorizedTagsCache.getAuthorizedTags(authorizationFile);
+    const idTag = tags[this.idTagIndex];
+    this.idTagIndex = this.idTagIndex === tags.length - 1 ? 0 : this.idTagIndex + 1;
+    return idTag;
+  }
+
+  private getConnectorAffinityIdTag(authorizationFile: string, connectorId: number): string {
+    const tags = this.chargingStation.authorizedTagsCache.getAuthorizedTags(authorizationFile);
+    this.idTagIndex = (this.chargingStation.index - 1 + (connectorId - 1)) % tags.length;
+    return tags[this.idTagIndex];
+  }
+
+  private getIdTag(connectorId: number): string {
+    const authorizationFile = ChargingStationUtils.getAuthorizationFile(
+      this.chargingStation.stationInfo
+    );
+    switch (this.configuration?.idTagDistribution) {
+      case IdTagDistribution.RANDOM:
+        return this.getRandomIdTag(authorizationFile);
+      case IdTagDistribution.ROUND_ROBIN:
+        return this.getRoundRobinIdTag(authorizationFile);
+      case IdTagDistribution.CONNECTOR_AFFINITY:
+        return this.getConnectorAffinityIdTag(authorizationFile, connectorId);
+      default:
+        return this.getRoundRobinIdTag(authorizationFile);
+    }
+  }
+
   private logPrefix(connectorId?: number): string {
     return Utils.logPrefix(
       ` ${this.chargingStation.stationInfo.chargingStationId} | ATG${
index 7c80067ccd93905b2e76cec7ac9a3641bd670ff6..cfb4eed6b309721fc5055998ca71526bc3366cb6 100644 (file)
@@ -2,7 +2,7 @@
 
 import path from 'path';
 import { fileURLToPath } from 'url';
-import { isMainThread } from 'worker_threads';
+import { type Worker, isMainThread } from 'worker_threads';
 
 import chalk from 'chalk';
 
@@ -177,6 +177,7 @@ export class Bootstrap {
             workerChoiceStrategy: Configuration.getWorker().poolStrategy,
           },
           messageHandler: this.messageHandler.bind(this) as (
+            this: Worker,
             msg: ChargingStationWorkerMessage<ChargingStationWorkerMessageData>
           ) => void,
         }
index 90bba2efd58b6a83d64a492d025445221f382f37..9fabd2a59a0ee69d4c25e301151a45d56ce081f8 100644 (file)
@@ -85,6 +85,7 @@ import type OCPPRequestService from './ocpp/OCPPRequestService';
 import SharedLRUCache from './SharedLRUCache';
 
 export default class ChargingStation {
+  public readonly index: number;
   public readonly templateFile: string;
   public stationInfo!: ChargingStationInfo;
   public started: boolean;
@@ -102,13 +103,13 @@ export default class ChargingStation {
   public powerDivider!: number;
   private starting: boolean;
   private stopping: boolean;
-  private readonly index: number;
   private configurationFile!: string;
   private configurationFileHash!: string;
   private connectorsConfigurationHash!: string;
   private ocppIncomingRequestService!: OCPPIncomingRequestService;
   private readonly messageBuffer: Set<string>;
   private configuredSupervisionUrl!: URL;
+  private configuredSupervisionUrlIndex!: number;
   private wsConnectionRestarted: boolean;
   private autoReconnectRetryCount: number;
   private templateFileWatcher!: fs.FSWatcher;
@@ -156,14 +157,6 @@ export default class ChargingStation {
     );
   }
 
-  public getRandomIdTag(): string {
-    const authorizationFile = ChargingStationUtils.getAuthorizationFile(this.stationInfo);
-    const index = Math.floor(
-      Utils.secureRandom() * this.authorizedTagsCache.getAuthorizedTags(authorizationFile).length
-    );
-    return this.authorizedTagsCache.getAuthorizedTags(authorizationFile)[index];
-  }
-
   public hasAuthorizedTags(): boolean {
     return !Utils.isEmptyArray(
       this.authorizedTagsCache.getAuthorizedTags(
@@ -880,10 +873,8 @@ export default class ChargingStation {
     stationInfo.resetTime = stationTemplate.resetTime
       ? stationTemplate.resetTime * 1000
       : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
-    const configuredMaxConnectors = ChargingStationUtils.getConfiguredNumberOfConnectors(
-      this.index,
-      stationTemplate
-    );
+    const configuredMaxConnectors =
+      ChargingStationUtils.getConfiguredNumberOfConnectors(stationTemplate);
     ChargingStationUtils.checkConfiguredMaxConnectors(
       configuredMaxConnectors,
       this.templateFile,
@@ -1645,7 +1636,7 @@ export default class ChargingStation {
   }
 
   // 0 for disabling
-  private getConnectionTimeout(): number | undefined {
+  private getConnectionTimeout(): number {
     if (
       ChargingStationConfigurationUtils.getConfigurationKey(
         this,
@@ -1665,7 +1656,7 @@ export default class ChargingStation {
   }
 
   // -1 for unlimited, 0 for disabling
-  private getAutoReconnectMaxRetries(): number | undefined {
+  private getAutoReconnectMaxRetries(): number {
     if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) {
       return this.stationInfo.autoReconnectMaxRetries;
     }
@@ -1676,7 +1667,7 @@ export default class ChargingStation {
   }
 
   // 0 for disabling
-  private getRegistrationMaxRetries(): number | undefined {
+  private getRegistrationMaxRetries(): number {
     if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) {
       return this.stationInfo.registrationMaxRetries;
     }
@@ -1928,39 +1919,34 @@ export default class ChargingStation {
       this.stationInfo.supervisionUrls ?? Configuration.getSupervisionUrls()
     );
     if (!Utils.isEmptyArray(supervisionUrls)) {
-      let urlIndex = 0;
       switch (Configuration.getSupervisionUrlDistribution()) {
         case SupervisionUrlDistribution.ROUND_ROBIN:
-          urlIndex = (this.index - 1) % supervisionUrls.length;
+          // FIXME
+          this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
           break;
         case SupervisionUrlDistribution.RANDOM:
-          // Get a random url
-          urlIndex = Math.floor(Utils.secureRandom() * supervisionUrls.length);
+          this.configuredSupervisionUrlIndex = Math.floor(
+            Utils.secureRandom() * supervisionUrls.length
+          );
           break;
-        case SupervisionUrlDistribution.SEQUENTIAL:
-          if (this.index <= supervisionUrls.length) {
-            urlIndex = this.index - 1;
-          } else {
-            logger.warn(
-              `${this.logPrefix()} No more configured supervision urls available, using the first one`
-            );
-          }
+        case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
+          this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
           break;
         default:
           logger.error(
             `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
-              SupervisionUrlDistribution.ROUND_ROBIN
+              SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
             }`
           );
-          urlIndex = (this.index - 1) % supervisionUrls.length;
+          this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
           break;
       }
-      return new URL(supervisionUrls[urlIndex]);
+      return new URL(supervisionUrls[this.configuredSupervisionUrlIndex]);
     }
     return new URL(supervisionUrls as string);
   }
 
-  private getHeartbeatInterval(): number | undefined {
+  private getHeartbeatInterval(): number {
     const HeartbeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
       this,
       StandardParametersKey.HeartbeatInterval
@@ -2003,7 +1989,7 @@ export default class ChargingStation {
     }
   }
 
-  private getReconnectExponentialDelay(): boolean | undefined {
+  private getReconnectExponentialDelay(): boolean {
     return !Utils.isUndefined(this.stationInfo.reconnectExponentialDelay)
       ? this.stationInfo.reconnectExponentialDelay
       : false;
index 370e030ccc5068eb457dc42dfbd3b999c4102864..24556120b4b9db9862dfae478ce3fcb468e2a309 100644 (file)
@@ -109,16 +109,13 @@ export class ChargingStationUtils {
     }
   }
 
-  public static getConfiguredNumberOfConnectors(
-    index: number,
-    stationTemplate: ChargingStationTemplate
-  ): number {
+  public static getConfiguredNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
     let configuredMaxConnectors: number;
-    if (!Utils.isEmptyArray(stationTemplate.numberOfConnectors)) {
+    if (Utils.isEmptyArray(stationTemplate.numberOfConnectors) === false) {
       const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
-      // Distribute evenly the number of connectors
-      configuredMaxConnectors = numberOfConnectors[(index - 1) % numberOfConnectors.length];
-    } else if (!Utils.isUndefined(stationTemplate.numberOfConnectors)) {
+      configuredMaxConnectors =
+        numberOfConnectors[Math.floor(Utils.secureRandom() * numberOfConnectors.length)];
+    } else if (Utils.isUndefined(stationTemplate.numberOfConnectors) === false) {
       configuredMaxConnectors = stationTemplate.numberOfConnectors as number;
     } else {
       configuredMaxConnectors = stationTemplate?.Connectors[0]
index 4f109bf5023206c826e55f880c278d576ccaafa3..58f1327ab862fa7b1042ab6495834bd1543fcbc3 100644 (file)
@@ -4,11 +4,8 @@ import { parentPort, workerData } from 'worker_threads';
 
 import { ThreadWorker } from 'poolifier';
 
-import {
-  ChargingStationWorkerData,
-  ChargingStationWorkerMessage,
-  ChargingStationWorkerMessageEvents,
-} from '../types/ChargingStationWorker';
+import type { ChargingStationWorkerData } from '../types/ChargingStationWorker';
+import { WorkerMessage, WorkerMessageEvents } from '../types/Worker';
 import Utils from '../utils/Utils';
 import WorkerConstants from '../worker/WorkerConstants';
 import ChargingStation from './ChargingStation';
@@ -33,8 +30,8 @@ if (ChargingStationUtils.workerPoolInUse()) {
  * Listen messages send by the main thread
  */
 function addMessageListener(): void {
-  parentPort?.on('message', (message: ChargingStationWorkerMessage<ChargingStationWorkerData>) => {
-    if (message.id === ChargingStationWorkerMessageEvents.START_WORKER_ELEMENT) {
+  parentPort?.on('message', (message: WorkerMessage<ChargingStationWorkerData>) => {
+    if (message.id === WorkerMessageEvents.START_WORKER_ELEMENT) {
       startChargingStation(message.data);
     }
   });
index fe11041afd2dd30fdef81fa6fa8aec427148c1be..9bf283d5e9e7330c98fac544e16da12eebfb93f8 100644 (file)
@@ -60,9 +60,13 @@ export class MessageChannelUtils {
         ({ transactionSetInterval, ...connectorStatusRest }) => connectorStatusRest
       ),
       ...(chargingStation.automaticTransactionGenerator && {
-        automaticTransactionGeneratorStatuses: [
-          ...chargingStation.automaticTransactionGenerator.connectorsStatus.values(),
-        ],
+        automaticTransactionGenerator: {
+          automaticTransactionGenerator:
+            chargingStation.automaticTransactionGenerator.configuration,
+          automaticTransactionGeneratorStatuses: [
+            ...chargingStation.automaticTransactionGenerator.connectorsStatus.values(),
+          ],
+        },
       }),
     };
   }
index c8ce3c9aaad48b31c311a8da6c9310caccd60c85..a91c9067e8c73f38afd3064e863be7f4988c7d60 100644 (file)
@@ -1,3 +1,9 @@
+export enum IdTagDistribution {
+  RANDOM = 'random',
+  ROUND_ROBIN = 'round-robin',
+  CONNECTOR_AFFINITY = 'connector-affinity',
+}
+
 export type AutomaticTransactionGeneratorConfiguration = {
   enable: boolean;
   minDuration: number;
@@ -8,6 +14,7 @@ export type AutomaticTransactionGeneratorConfiguration = {
   stopAfterHours: number;
   stopOnConnectionFailure: boolean;
   requireAuthorize?: boolean;
+  idTagDistribution?: IdTagDistribution;
 };
 
 export type Status = {
@@ -31,5 +38,5 @@ export type Status = {
 
 export type ChargingStationAutomaticTransactionGeneratorConfiguration = {
   automaticTransactionGenerator?: AutomaticTransactionGeneratorConfiguration;
-  automaticTransactionGeneratorStatus?: Status;
+  automaticTransactionGeneratorStatuses?: Status[];
 };
index ec6e92b52bf1243ab4f1c77b13265b0ca2239a0a..ac13f441a921d50eced020fd4defe5c122f77993 100644 (file)
@@ -1,6 +1,6 @@
 import type { WebSocket } from 'ws';
 
-import type { Status } from './AutomaticTransactionGenerator';
+import type { ChargingStationAutomaticTransactionGeneratorConfiguration } from './AutomaticTransactionGenerator';
 import type { ChargingStationInfo } from './ChargingStationInfo';
 import type { ConnectorStatus } from './ConnectorStatus';
 import type { JsonObject } from './JsonType';
@@ -28,7 +28,7 @@ export interface ChargingStationData extends WorkerData {
     | typeof WebSocket.CLOSED;
   bootNotificationResponse: BootNotificationResponse;
   connectors: ConnectorStatus[];
-  automaticTransactionGeneratorStatuses?: Status[];
+  automaticTransactionGenerator?: ChargingStationAutomaticTransactionGeneratorConfiguration;
 }
 
 enum ChargingStationMessageEvents {
@@ -47,6 +47,9 @@ export const ChargingStationWorkerMessageEvents = {
 
 export type ChargingStationWorkerMessageData = ChargingStationData | Statistics;
 
-export type ChargingStationWorkerMessage<T extends WorkerData> = Omit<WorkerMessage<T>, 'id'> & {
+export type ChargingStationWorkerMessage<T extends ChargingStationWorkerMessageData> = Omit<
+  WorkerMessage<T>,
+  'id'
+> & {
   id: ChargingStationWorkerMessageEvents;
 };
index ba9fb14ce993430379facda2f77d376e00e48a15..58372a3882a5cb064ae7df9c20748c0693ce18e9 100644 (file)
@@ -11,7 +11,7 @@ export type ServerOptions = ListenOptions;
 export enum SupervisionUrlDistribution {
   ROUND_ROBIN = 'round-robin',
   RANDOM = 'random',
-  SEQUENTIAL = 'sequential',
+  CHARGING_STATION_AFFINITY = 'charging-station-affinity',
 }
 
 export type StationTemplateUrl = {
index c2594b9a932cf9b6fcc903c23e37a715143523cc..29f70aa4d694e64075934ebf398eaa6cdc92a34e 100644 (file)
@@ -15,7 +15,7 @@ export type WorkerOptions = {
   poolMinSize?: number;
   elementsPerWorker?: number;
   poolOptions?: PoolOptions<Worker>;
-  messageHandler?: (message: unknown) => void | Promise<void>;
+  messageHandler?: (this: Worker, message: unknown) => void | Promise<void>;
 };
 
 export type WorkerData = Record<string, unknown>;