Unify supervision urls setup and distribution to charging stations
authorJérôme Benoit <jerome.benoit@sap.com>
Tue, 1 Feb 2022 14:20:54 +0000 (15:20 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Tue, 1 Feb 2022 14:20:54 +0000 (15:20 +0100)
Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
README.md
src/assets/config-template.json
src/charging-station/ChargingStation.ts
src/types/ChargingStationTemplate.ts
src/types/ConfigurationData.ts
src/utils/Configuration.ts

index 6dc4574f313050c0ab35793c625f5bcfe6aa7f56..9f1156e75fe46147311288736462254b850599e6 100644 (file)
--- a/README.md
+++ b/README.md
@@ -49,8 +49,8 @@ But the modifications to test have to be done to the files in the build result d
 
 Key | Value(s) | Default Value | Value type | Description 
 --- | -------| --------------| ---------- | ------------
-supervisionUrls | | [] | string[] |  array of connection URIs to OCPP-J servers
-distributeStationsToTenantsEqually | true/false | true | boolean | distribute charging stations uniformly to the OCPP-J servers
+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
 workerProcess | workerSet/staticPool/dynamicPool | workerSet | string | worker threads process type
 workerStartDelay | | 500 | integer | milliseconds to wait at charging station worker threads startup
 workerPoolMinSize | | 4 | integer | worker threads pool minimum number of threads
@@ -84,7 +84,7 @@ stationTemplateUrls | | {}[] | { file: string; numberOfStations: number; }[] | a
 
 Key | Value(s) | Default Value | Value type | Description 
 --- | -------| --------------| ---------- | ------------
-supervisionUrl | | '' | string | connection URI to OCPP-J server
+supervisionUrls | | '' | string \| string[] | string or array of connection URIs to OCPP-J servers. It has priority over the global configuration parameter
 supervisionUser | | '' | string | basic HTTP authentication user to OCPP-J server
 supervisionPassword | | '' | string | basic HTTP authentication password to OCPP-J server
 supervisionUrlOcppConfiguration | true/false | false | boolean | Allow supervision URL configuration via a vendor OCPP parameter key
index a0c0526abbf1a0c5c9266310cd17579d0ea80cb0..4520a2d49fed76f6c21143dd08c4f96551154c2c 100644 (file)
@@ -2,7 +2,7 @@
   "supervisionUrls": [
     "ws://localhost:8010/OCPP16/5be7fb271014d90008992f06"
   ],
-  "distributeStationsToTenantsEqually": true,
+  "supervisionUrlDistribution": "sequential",
   "performanceStorage": {
     "enabled": true,
     "type": "jsonfile"
index ad4dac388a293b56243982184dd1f40848b7fbf3..4f6c7771283f865bb44d700541fb5fe42e0fbf36 100644 (file)
@@ -31,6 +31,7 @@ import { OCPPVersion } from '../types/ocpp/OCPPVersion';
 import PerformanceStatistics from '../performance/PerformanceStatistics';
 import { SampledValueTemplate } from '../types/MeasurandPerPhaseSampledValueTemplates';
 import { StopTransactionReason } from '../types/ocpp/Transaction';
+import { SupervisionUrlDistribution } from '../types/ConfigurationData';
 import { URL } from 'url';
 import Utils from '../utils/Utils';
 import crypto from 'crypto';
@@ -461,6 +462,10 @@ export default class ChargingStation {
     } catch (error) {
       FileUtils.handleFileException(this.logPrefix(), 'Template', this.stationTemplateFile, error as NodeJS.ErrnoException);
     }
+    const chargingStationId = this.getChargingStationId(stationTemplateFromFile);
+    // Deprecation template keys section
+    this.warnDeprecatedTemplateKey(stationTemplateFromFile, 'supervisionUrl', chargingStationId, 'Use \'supervisionUrls\' instead');
+    this.convertDeprecatedTemplateKey(stationTemplateFromFile, 'supervisionUrl', 'supervisionUrls');
     const stationInfo: ChargingStationInfo = stationTemplateFromFile ?? {} as ChargingStationInfo;
     stationInfo.wsOptions = stationTemplateFromFile?.wsOptions ?? {};
     if (!Utils.isEmptyArray(stationTemplateFromFile.power)) {
@@ -477,7 +482,7 @@ export default class ChargingStation {
     }
     delete stationInfo.power;
     delete stationInfo.powerUnit;
-    stationInfo.chargingStationId = this.getChargingStationId(stationTemplateFromFile);
+    stationInfo.chargingStationId = chargingStationId;
     stationInfo.resetTime = stationTemplateFromFile.resetTime ? stationTemplateFromFile.resetTime * 1000 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
     return stationInfo;
   }
@@ -933,17 +938,45 @@ export default class ChargingStation {
     }
   }
 
+  private warnDeprecatedTemplateKey(template: ChargingStationTemplate, key: string, chargingStationId: string, logMsgToAppend = ''): void {
+    if (!Utils.isUndefined(template[key])) {
+      logger.warn(`${Utils.logPrefix(` ${chargingStationId} |`)} Deprecated template key '${key}' usage in file '${this.stationTemplateFile}'${logMsgToAppend && '. ' + logMsgToAppend}`);
+    }
+  }
+
+  private convertDeprecatedTemplateKey(template: ChargingStationTemplate, deprecatedKey: string, key: string): void {
+    if (!Utils.isUndefined(template[deprecatedKey])) {
+      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+      template[key] = template[deprecatedKey];
+      delete template[deprecatedKey];
+    }
+  }
+
   private getConfiguredSupervisionUrl(): URL {
-    const supervisionUrls = Utils.cloneObject<string | string[]>(this.stationInfo.supervisionUrl ?? Configuration.getSupervisionUrls());
-    let indexUrl = 0;
+    const supervisionUrls = Utils.cloneObject<string | string[]>(this.stationInfo.supervisionUrls ?? Configuration.getSupervisionUrls());
     if (!Utils.isEmptyArray(supervisionUrls)) {
-      if (Configuration.getDistributeStationsToTenantsEqually()) {
-        indexUrl = this.index % supervisionUrls.length;
-      } else {
-        // Get a random url
-        indexUrl = Math.floor(Utils.secureRandom() * supervisionUrls.length);
+      let urlIndex = 0;
+      switch (Configuration.getSupervisionUrlDistribution()) {
+        case SupervisionUrlDistribution.ROUND_ROBIN:
+          urlIndex = (this.index - 1) % supervisionUrls.length;
+          break;
+        case SupervisionUrlDistribution.RANDOM:
+          // Get a random url
+          urlIndex = 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`);
+          }
+          break;
+        default:
+          logger.error(`${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}', defaulting to ${SupervisionUrlDistribution.ROUND_ROBIN}`);
+          urlIndex = (this.index - 1) % supervisionUrls.length;
+          break;
       }
-      return new URL(supervisionUrls[indexUrl]);
+      return new URL(supervisionUrls[urlIndex]);
     }
     return new URL(supervisionUrls as string);
   }
index 5d553318576cc7b3980b7e8c8b2ad2b9c06dc0e0..c06468e91f6a00c90018725fa900755aa1da8b99 100644 (file)
@@ -35,7 +35,7 @@ export interface AutomaticTransactionGenerator {
 }
 
 export default interface ChargingStationTemplate {
-  supervisionUrl?: string;
+  supervisionUrls?: string | string[];
   supervisionUrlOcppConfiguration?: boolean;
   supervisionUrlOcppKey?: string;
   supervisionUser?: string;
index 51eb045a19d717b7ce3e4cf1d5015a42256dc539..56ee0354510cca07327b6a6ce48973f1a8ab9dcd 100644 (file)
@@ -4,6 +4,12 @@ import type { WorkerChoiceStrategy } from 'poolifier';
 import { WorkerProcessType } from './Worker';
 import { level } from 'winston';
 
+export enum SupervisionUrlDistribution {
+  ROUND_ROBIN = 'round-robin',
+  RANDOM = 'random',
+  SEQUENTIAL = 'sequential',
+}
+
 export interface StationTemplateUrl {
   file: string;
   numberOfStations: number;
@@ -21,12 +27,12 @@ export interface StorageConfiguration {
 }
 
 export default interface ConfigurationData {
-  supervisionUrls?: string[];
+  supervisionUrls?: string | string[];
+  supervisionUrlDistribution?: SupervisionUrlDistribution;
   stationTemplateUrls: StationTemplateUrl[];
   uiWebSocketServer?: UIWebSocketServerConfiguration;
   performanceStorage?: StorageConfiguration;
   autoReconnectMaxRetries?: number;
-  distributeStationsToTenantsEqually?: boolean;
   workerProcess?: WorkerProcessType;
   workerStartDelay?: number;
   workerPoolMinSize?: number;
index 61fe300b30844f82961e31b6b5ea9c0ccd43c577..b1bc6e3adbe4a6662a37a902fb30ba4ca31af656 100644 (file)
@@ -1,4 +1,4 @@
-import ConfigurationData, { StationTemplateUrl, StorageConfiguration, UIWebSocketServerConfiguration } from '../types/ConfigurationData';
+import ConfigurationData, { StationTemplateUrl, StorageConfiguration, SupervisionUrlDistribution, UIWebSocketServerConfiguration } from '../types/ConfigurationData';
 
 import Constants from './Constants';
 import { ServerOptions } from 'ws';
@@ -150,16 +150,17 @@ export default class Configuration {
     return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logErrorFile') ? Configuration.getConfig().logErrorFile : 'error.log';
   }
 
-  static getSupervisionUrls(): string[] {
+  static getSupervisionUrls(): string | string[] {
     Configuration.warnDeprecatedConfigurationKey('supervisionURLs', null, 'Use \'supervisionUrls\' instead');
     !Configuration.isUndefined(Configuration.getConfig()['supervisionURLs']) && (Configuration.getConfig().supervisionUrls = Configuration.getConfig()['supervisionURLs'] as string[]);
     // Read conf
     return Configuration.getConfig().supervisionUrls;
   }
 
-  static getDistributeStationsToTenantsEqually(): boolean {
-    Configuration.warnDeprecatedConfigurationKey('distributeStationToTenantEqually', null, 'Use \'distributeStationsToTenantsEqually\' instead');
-    return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'distributeStationsToTenantsEqually') ? Configuration.getConfig().distributeStationsToTenantsEqually : true;
+  static getSupervisionUrlDistribution(): SupervisionUrlDistribution {
+    Configuration.warnDeprecatedConfigurationKey('distributeStationToTenantEqually', null, 'Use \'supervisionUrlDistribution\' instead');
+    Configuration.warnDeprecatedConfigurationKey('distributeStationsToTenantsEqually', null, 'Use \'supervisionUrlDistribution\' instead');
+    return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'supervisionUrlDistribution') ? Configuration.getConfig().supervisionUrlDistribution : SupervisionUrlDistribution.ROUND_ROBIN;
   }
 
   private static logPrefix(): string {