]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor: reduce code duplication across OCPP services, UI server, and configuration
authorJérôme Benoit <jerome.benoit@sap.com>
Tue, 31 Mar 2026 14:41:58 +0000 (16:41 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Tue, 31 Mar 2026 14:42:24 +0000 (16:42 +0200)
- Centralize AJV instantiation into createAjv() factory in OCPPServiceUtils
- Centralize payload validator config creation into createPayloadConfigs() generic
  factory, simplifying OCPP16/20ServiceUtils from multi-line .map() to one-liners
- Move rate limiter creation from module-level duplicates in UIHttpServer and
  UIMCPServer to a shared instance property on AbstractUIServer
- Extract 200-line checkDeprecatedConfigurationKeys() and helpers from
  Configuration.ts into dedicated ConfigurationMigration.ts module

src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts
src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts
src/charging-station/ocpp/OCPPIncomingRequestService.ts
src/charging-station/ocpp/OCPPRequestService.ts
src/charging-station/ocpp/OCPPResponseService.ts
src/charging-station/ocpp/OCPPServiceUtils.ts
src/charging-station/ui-server/AbstractUIServer.ts
src/charging-station/ui-server/UIHttpServer.ts
src/charging-station/ui-server/UIMCPServer.ts
src/utils/Configuration.ts
src/utils/ConfigurationMigration.ts [new file with mode: 0644]

index 230d1c8f72ac8f165369a11bc1721e2dd20f655d..d0fc1e263a4b33e5201fe145602fdd9b8b962977 100644 (file)
@@ -60,8 +60,8 @@ import {
   buildEmptyMeterValue,
   buildMeterValue,
   buildSampledValue,
+  createPayloadConfigs,
   getSampledValueTemplate,
-  PayloadValidatorConfig,
   PayloadValidatorOptions,
   sendAndSetConnectorStatus,
 } from '../OCPPServiceUtils.js'
@@ -555,11 +555,7 @@ export class OCPP16ServiceUtils {
   public static createIncomingRequestPayloadConfigs = (): [
     OCPP16IncomingRequestCommand,
     { schemaPath: string }
-  ][] =>
-    OCPP16ServiceUtils.incomingRequestSchemaNames.map(([command, schemaBase]) => [
-      command,
-      PayloadValidatorConfig(`${schemaBase}.json`),
-    ])
+  ][] => createPayloadConfigs(OCPP16ServiceUtils.incomingRequestSchemaNames, '.json')
 
   /**
    * OCPP 1.6 Incoming Request Response Service validator configurations
@@ -568,11 +564,7 @@ export class OCPP16ServiceUtils {
   public static createIncomingRequestResponsePayloadConfigs = (): [
     OCPP16IncomingRequestCommand,
     { schemaPath: string }
-  ][] =>
-    OCPP16ServiceUtils.incomingRequestSchemaNames.map(([command, schemaBase]) => [
-      command,
-      PayloadValidatorConfig(`${schemaBase}Response.json`),
-    ])
+  ][] => createPayloadConfigs(OCPP16ServiceUtils.incomingRequestSchemaNames, 'Response.json')
 
   /**
    * Factory options for OCPP 1.6 payload validators
@@ -595,11 +587,7 @@ export class OCPP16ServiceUtils {
   public static createRequestPayloadConfigs = (): [
     OCPP16RequestCommand,
     { schemaPath: string }
-  ][] =>
-    OCPP16ServiceUtils.outgoingRequestSchemaNames.map(([command, schemaBase]) => [
-      command,
-      PayloadValidatorConfig(`${schemaBase}.json`),
-    ])
+  ][] => createPayloadConfigs(OCPP16ServiceUtils.outgoingRequestSchemaNames, '.json')
 
   /**
    * OCPP 1.6 Response Service validator configurations
@@ -608,11 +596,7 @@ export class OCPP16ServiceUtils {
   public static createResponsePayloadConfigs = (): [
     OCPP16RequestCommand,
     { schemaPath: string }
-  ][] =>
-    OCPP16ServiceUtils.outgoingRequestSchemaNames.map(([command, schemaBase]) => [
-      command,
-      PayloadValidatorConfig(`${schemaBase}Response.json`),
-    ])
+  ][] => createPayloadConfigs(OCPP16ServiceUtils.outgoingRequestSchemaNames, 'Response.json')
 
   /**
    * Checks whether a connector or the charging station has a valid reservation for the given idTag.
index 391f0ba10074a77e802497734796ae287e560a32..de52832cfa19d00c19145518cac296121636822f 100644 (file)
@@ -46,7 +46,7 @@ import {
 import { buildConfigKey, getConfigurationKey } from '../../ConfigurationKeyUtils.js'
 import {
   buildMeterValue,
-  PayloadValidatorConfig,
+  createPayloadConfigs,
   PayloadValidatorOptions,
   sendAndSetConnectorStatus,
 } from '../OCPPServiceUtils.js'
@@ -223,11 +223,7 @@ export class OCPP20ServiceUtils {
   public static createIncomingRequestPayloadConfigs = (): [
     OCPP20IncomingRequestCommand,
     { schemaPath: string }
-  ][] =>
-    OCPP20ServiceUtils.incomingRequestSchemaNames.map(([command, schemaBase]) => [
-      command,
-      PayloadValidatorConfig(`${schemaBase}Request.json`),
-    ])
+  ][] => createPayloadConfigs(OCPP20ServiceUtils.incomingRequestSchemaNames, 'Request.json')
 
   /**
    * Configuration for OCPP 2.0 Incoming Request Response validators
@@ -236,11 +232,7 @@ export class OCPP20ServiceUtils {
   public static createIncomingRequestResponsePayloadConfigs = (): [
     OCPP20IncomingRequestCommand,
     { schemaPath: string }
-  ][] =>
-    OCPP20ServiceUtils.incomingRequestSchemaNames.map(([command, schemaBase]) => [
-      command,
-      PayloadValidatorConfig(`${schemaBase}Response.json`),
-    ])
+  ][] => createPayloadConfigs(OCPP20ServiceUtils.incomingRequestSchemaNames, 'Response.json')
 
   /**
    * Factory options for OCPP 2.0 payload validators
@@ -263,11 +255,7 @@ export class OCPP20ServiceUtils {
   public static createRequestPayloadConfigs = (): [
     OCPP20RequestCommand,
     { schemaPath: string }
-  ][] =>
-    OCPP20ServiceUtils.outgoingRequestSchemaNames.map(([command, schemaBase]) => [
-      command,
-      PayloadValidatorConfig(`${schemaBase}Request.json`),
-    ])
+  ][] => createPayloadConfigs(OCPP20ServiceUtils.outgoingRequestSchemaNames, 'Request.json')
 
   /**
    * OCPP 2.0 Response Service validator configurations
@@ -276,11 +264,7 @@ export class OCPP20ServiceUtils {
   public static createResponsePayloadConfigs = (): [
     OCPP20RequestCommand,
     { schemaPath: string }
-  ][] =>
-    OCPP20ServiceUtils.outgoingRequestSchemaNames.map(([command, schemaBase]) => [
-      command,
-      PayloadValidatorConfig(`${schemaBase}Response.json`),
-    ])
+  ][] => createPayloadConfigs(OCPP20ServiceUtils.outgoingRequestSchemaNames, 'Response.json')
 
   /**
    * Enforce ItemsPerMessage and BytesPerMessage limits on request data.
index c3a13f7bb8de6cb4232388240d61f1bad5334e78..ff9f3c9bcca065aa447ed44ff4c33c3be3dfc98e 100644 (file)
@@ -1,5 +1,6 @@
-import _Ajv, { type ValidateFunction } from 'ajv'
-import _ajvFormats from 'ajv-formats'
+import type { ValidateFunction } from 'ajv'
+import type _Ajv from 'ajv'
+
 import { EventEmitter } from 'node:events'
 
 import { type ChargingStation } from '../../charging-station/index.js'
@@ -12,12 +13,9 @@ import {
   type OCPPVersion,
 } from '../../types/index.js'
 import { isAsyncFunction, logger } from '../../utils/index.js'
-import { ajvErrorsToErrorType } from './OCPPServiceUtils.js'
+import { ajvErrorsToErrorType, createAjv } from './OCPPServiceUtils.js'
 
 type Ajv = _Ajv.default
-// eslint-disable-next-line @typescript-eslint/no-redeclare
-const Ajv = _Ajv.default
-const ajvFormats = _ajvFormats.default
 
 const moduleName = 'OCPPIncomingRequestService'
 
@@ -47,11 +45,7 @@ export abstract class OCPPIncomingRequestService extends EventEmitter {
   protected constructor (version: OCPPVersion) {
     super()
     this.version = version
-    this.ajv = new Ajv({
-      keywords: ['javaType'],
-      multipleOfPrecision: 2,
-    })
-    ajvFormats(this.ajv)
+    this.ajv = createAjv()
     this.incomingRequestHandler = this.incomingRequestHandler.bind(this)
     this.validateIncomingRequestPayload = this.validateIncomingRequestPayload.bind(this)
   }
index c4cab42d899d5a15e63c8e9071ceafba4efc21ff..ca104d396390aabd49002e8931283ec5711d90f7 100644 (file)
@@ -1,5 +1,5 @@
-import _Ajv, { type ValidateFunction } from 'ajv'
-import _ajvFormats from 'ajv-formats'
+import type { ValidateFunction } from 'ajv'
+import type _Ajv from 'ajv'
 
 import type { ChargingStation } from '../../charging-station/index.js'
 import type { OCPPResponseService } from './OCPPResponseService.js'
@@ -32,12 +32,9 @@ import {
   logger,
 } from '../../utils/index.js'
 import { OCPPConstants } from './OCPPConstants.js'
-import { ajvErrorsToErrorType, convertDateToISOString } from './OCPPServiceUtils.js'
+import { ajvErrorsToErrorType, convertDateToISOString, createAjv } from './OCPPServiceUtils.js'
 
 type Ajv = _Ajv.default
-// eslint-disable-next-line @typescript-eslint/no-redeclare
-const Ajv = _Ajv.default
-const ajvFormats = _ajvFormats.default
 
 const moduleName = 'OCPPRequestService'
 
@@ -60,11 +57,7 @@ export abstract class OCPPRequestService {
 
   protected constructor (version: OCPPVersion, ocppResponseService: OCPPResponseService) {
     this.version = version
-    this.ajv = new Ajv({
-      keywords: ['javaType'],
-      multipleOfPrecision: 2,
-    })
-    ajvFormats(this.ajv)
+    this.ajv = createAjv()
     this.ocppResponseService = ocppResponseService
     this.requestHandler = this.requestHandler.bind(this)
     this.sendMessage = this.sendMessage.bind(this)
index 96d27f298d74fe9d960292044272389601fc54c1..ad9674f23f171c5b805f231a62651431e489f001 100644 (file)
@@ -1,5 +1,5 @@
-import _Ajv, { type ValidateFunction } from 'ajv'
-import _ajvFormats from 'ajv-formats'
+import type { ValidateFunction } from 'ajv'
+import type _Ajv from 'ajv'
 
 import type { ChargingStation } from '../../charging-station/index.js'
 
@@ -13,12 +13,9 @@ import {
   type ResponseHandler,
 } from '../../types/index.js'
 import { Constants, isAsyncFunction, logger } from '../../utils/index.js'
-import { ajvErrorsToErrorType } from './OCPPServiceUtils.js'
+import { ajvErrorsToErrorType, createAjv } from './OCPPServiceUtils.js'
 
 type Ajv = _Ajv.default
-// eslint-disable-next-line @typescript-eslint/no-redeclare
-const Ajv = _Ajv.default
-const ajvFormats = _ajvFormats.default
 
 const moduleName = 'OCPPResponseService'
 
@@ -41,16 +38,8 @@ export abstract class OCPPResponseService {
 
   protected constructor (version: OCPPVersion) {
     this.version = version
-    this.ajv = new Ajv({
-      keywords: ['javaType'],
-      multipleOfPrecision: 2,
-    })
-    ajvFormats(this.ajv)
-    this.ajvIncomingRequest = new Ajv({
-      keywords: ['javaType'],
-      multipleOfPrecision: 2,
-    })
-    ajvFormats(this.ajvIncomingRequest)
+    this.ajv = createAjv()
+    this.ajvIncomingRequest = createAjv()
     this.responseHandler = this.responseHandler.bind(this)
     this.validateResponsePayload = this.validateResponsePayload.bind(this)
   }
index 81f42a0103d8b6c08fc987d91872b05892dcb9eb..0b0a858f75513586954cbe443048c6e30298d414 100644 (file)
@@ -1,4 +1,5 @@
 import _Ajv, { type ErrorObject, type JSONSchemaType, type ValidateFunction } from 'ajv'
+import _ajvFormats from 'ajv-formats'
 import { isDate } from 'date-fns'
 import { randomInt } from 'node:crypto'
 import { readFileSync } from 'node:fs'
@@ -69,6 +70,16 @@ const moduleName = 'OCPPServiceUtils'
 type Ajv = _Ajv.default
 // eslint-disable-next-line @typescript-eslint/no-redeclare
 const Ajv = _Ajv.default
+const ajvFormats = _ajvFormats.default
+
+export const createAjv = (): Ajv => {
+  const ajv = new Ajv({
+    keywords: ['javaType'],
+    multipleOfPrecision: 2,
+  })
+  ajvFormats(ajv)
+  return ajv
+}
 
 interface MultiPhaseMeasurandData {
   perPhaseTemplates: MeasurandPerPhaseSampledValueTemplates
@@ -2081,16 +2092,27 @@ export function isRequestCommandSupported (
   return false
 }
 
-/**
- * Configuration for a single payload validator.
- * @param schemaPath - Path to the JSON schema file
- * @returns Configuration object for payload validator creation
- */
-export const PayloadValidatorConfig = (schemaPath: string) =>
+const PayloadValidatorConfig = (schemaPath: string) =>
   ({
     schemaPath,
   }) as const
 
+/**
+ * Maps schema name tuples to payload validator config tuples with the given suffix.
+ * @param schemaNames - Array of `[command, schemaBase]` tuples
+ * @param schemaSuffix - File suffix appended to each schema base (e.g. `Request.json`)
+ * @returns Array of `[command, config]` tuples for payload validator map construction
+ */
+export function createPayloadConfigs<Command> (
+  schemaNames: readonly [Command, string][],
+  schemaSuffix: string
+): [Command, { schemaPath: string }][] {
+  return schemaNames.map(([command, schemaBase]) => [
+    command,
+    PayloadValidatorConfig(`${schemaBase}${schemaSuffix}`),
+  ])
+}
+
 /**
  * Options for payload validator creation.
  * @param ocppVersion - The OCPP version
index 4e1c418ef0d34edf72174356839ca535d6b5a88b..8654901be35891a81300e92407d1b1c3cc0022ec 100644 (file)
@@ -23,13 +23,19 @@ import {
 } from '../../types/index.js'
 import { isEmpty, isNotEmptyString, logger, logPrefix } from '../../utils/index.js'
 import { UIServiceFactory } from './ui-services/UIServiceFactory.js'
-import { isValidCredential } from './UIServerSecurity.js'
+import {
+  createRateLimiter,
+  DEFAULT_RATE_LIMIT,
+  DEFAULT_RATE_WINDOW,
+  isValidCredential,
+} from './UIServerSecurity.js'
 import { getUsernameAndPasswordFromAuthorizationToken } from './UIServerUtils.js'
 
 const moduleName = 'AbstractUIServer'
 
 export abstract class AbstractUIServer {
   protected readonly httpServer: Http2Server | Server
+  protected readonly rateLimiter: ReturnType<typeof createRateLimiter>
   protected readonly responseHandlers: Map<UUIDv4, ServerResponse | WebSocket>
 
   protected abstract readonly uiServerType: string
@@ -62,6 +68,7 @@ export abstract class AbstractUIServer {
         )
     }
     this.responseHandlers = new Map<UUIDv4, ServerResponse | WebSocket>()
+    this.rateLimiter = createRateLimiter(DEFAULT_RATE_LIMIT, DEFAULT_RATE_WINDOW)
     this.uiServices = new Map<ProtocolVersion, AbstractUIService>()
   }
 
index a07f51d3212c75bc712a518d35dc955a13678dae..8dce2ca3755fe6148278aa74c9b0d91fe8d1b1d2 100644 (file)
@@ -23,18 +23,13 @@ import { generateUUID, getErrorMessage, JSONStringify, logger } from '../../util
 import { AbstractUIServer } from './AbstractUIServer.js'
 import {
   createBodySizeLimiter,
-  createRateLimiter,
   DEFAULT_COMPRESSION_THRESHOLD,
   DEFAULT_MAX_PAYLOAD_SIZE,
-  DEFAULT_RATE_LIMIT,
-  DEFAULT_RATE_WINDOW,
 } from './UIServerSecurity.js'
 import { HttpMethod, isProtocolAndVersionSupported } from './UIServerUtils.js'
 
 const moduleName = 'UIHttpServer'
 
-const rateLimiter = createRateLimiter(DEFAULT_RATE_LIMIT, DEFAULT_RATE_WINDOW)
-
 /**
  * @deprecated Use UIMCPServer (ApplicationProtocol.MCP) instead. Will be removed in a future major version.
  */
@@ -112,7 +107,7 @@ export class UIHttpServer extends AbstractUIServer {
   private requestListener (req: IncomingMessage, res: ServerResponse): void {
     // Rate limiting check
     const clientIp = req.socket.remoteAddress ?? 'unknown'
-    if (!rateLimiter(clientIp)) {
+    if (!this.rateLimiter(clientIp)) {
       res
         .writeHead(StatusCodes.TOO_MANY_REQUESTS, {
           'Content-Type': 'text/plain',
index 03f6381ef4764ce5364eee2de0f070f735247883..695bb3c362895b4a485a6388eb6b1106047e61c2 100644 (file)
@@ -31,20 +31,13 @@ import {
   registerMCPResources,
   registerMCPSchemaResources,
 } from './mcp/index.js'
-import {
-  createRateLimiter,
-  DEFAULT_MAX_PAYLOAD_SIZE,
-  DEFAULT_RATE_LIMIT,
-  DEFAULT_RATE_WINDOW,
-} from './UIServerSecurity.js'
+import { DEFAULT_MAX_PAYLOAD_SIZE } from './UIServerSecurity.js'
 import { HttpMethod } from './UIServerUtils.js'
 
 const moduleName = 'UIMCPServer'
 
 const MCP_TOOL_TIMEOUT_MS = 30_000
 
-const rateLimiter = createRateLimiter(DEFAULT_RATE_LIMIT, DEFAULT_RATE_WINDOW)
-
 export class UIMCPServer extends AbstractUIServer {
   protected override readonly uiServerType = 'UI MCP Server'
 
@@ -122,7 +115,7 @@ export class UIMCPServer extends AbstractUIServer {
       }
 
       const clientIp = req.socket.remoteAddress ?? 'unknown'
-      if (!rateLimiter(clientIp)) {
+      if (!this.rateLimiter(clientIp)) {
         res.writeHead(429, { 'Content-Type': 'text/plain' }).end('429 Too Many Requests')
         return
       }
index a7745520a9797b8725768d715749706687dca576..cc944a2ab031a8d6fa0eba2c7fd29f2edeaaecf9 100644 (file)
@@ -26,6 +26,7 @@ import {
   DEFAULT_WORKER_START_DELAY,
   WorkerProcessType,
 } from '../worker/index.js'
+import { checkDeprecatedConfigurationKeys } from './ConfigurationMigration.js'
 import {
   buildPerformanceUriFilePath,
   checkWorkerElementsPerWorker,
@@ -161,9 +162,9 @@ export class Configuration {
   }
 
   public static getStationTemplateUrls (): StationTemplateUrl[] | undefined {
-    const checkDeprecatedConfigurationKeysOnce = once(
-      Configuration.checkDeprecatedConfigurationKeys.bind(Configuration)
-    )
+    const checkDeprecatedConfigurationKeysOnce = once(() => {
+      checkDeprecatedConfigurationKeys(Configuration.getConfigurationData())
+    })
     checkDeprecatedConfigurationKeysOnce()
     return Configuration.getConfigurationData()?.stationTemplateUrls
   }
@@ -351,177 +352,6 @@ export class Configuration {
     }
   }
 
-  private static checkDeprecatedConfigurationKeys (): void {
-    const deprecatedKeys: [string, ConfigurationSection | undefined, string][] = [
-      // connection timeout
-      [
-        'autoReconnectTimeout',
-        undefined,
-        "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
-      ],
-      [
-        'connectionTimeout',
-        undefined,
-        "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
-      ],
-      // connection retries
-      ['autoReconnectMaxRetries', undefined, 'Use it in charging station template instead'],
-      // station template url(s)
-      ['stationTemplateURLs', undefined, "Use 'stationTemplateUrls' instead"],
-      // supervision url(s)
-      ['supervisionURLs', undefined, "Use 'supervisionUrls' instead"],
-      // supervision urls distribution
-      ['distributeStationToTenantEqually', undefined, "Use 'supervisionUrlDistribution' instead"],
-      ['distributeStationsToTenantsEqually', undefined, "Use 'supervisionUrlDistribution' instead"],
-      // worker section
-      [
-        'useWorkerPool',
-        undefined,
-        `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
-      ],
-      [
-        'workerProcess',
-        undefined,
-        `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
-      ],
-      [
-        'workerStartDelay',
-        undefined,
-        `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`,
-      ],
-      [
-        'chargingStationsPerWorker',
-        undefined,
-        `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`,
-      ],
-      [
-        'elementAddDelay',
-        undefined,
-        `Use '${ConfigurationSection.worker}' section to define the worker's element add delay instead`,
-      ],
-      [
-        'workerPoolMinSize',
-        undefined,
-        `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`,
-      ],
-      [
-        'workerPoolSize',
-        undefined,
-        `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
-      ],
-      [
-        'workerPoolMaxSize',
-        undefined,
-        `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
-      ],
-      [
-        'workerPoolStrategy',
-        undefined,
-        `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`,
-      ],
-      ['poolStrategy', ConfigurationSection.worker, 'Not publicly exposed to end users'],
-      ['elementStartDelay', ConfigurationSection.worker, "Use 'elementAddDelay' instead"],
-      // log section
-      [
-        'logEnabled',
-        undefined,
-        `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
-      ],
-      [
-        'logFile',
-        undefined,
-        `Use '${ConfigurationSection.log}' section to define the log file instead`,
-      ],
-      [
-        'logErrorFile',
-        undefined,
-        `Use '${ConfigurationSection.log}' section to define the log error file instead`,
-      ],
-      [
-        'logConsole',
-        undefined,
-        `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
-      ],
-      [
-        'logStatisticsInterval',
-        undefined,
-        `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
-      ],
-      [
-        'logLevel',
-        undefined,
-        `Use '${ConfigurationSection.log}' section to define the log level instead`,
-      ],
-      [
-        'logFormat',
-        undefined,
-        `Use '${ConfigurationSection.log}' section to define the log format instead`,
-      ],
-      [
-        'logRotate',
-        undefined,
-        `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
-      ],
-      [
-        'logMaxFiles',
-        undefined,
-        `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
-      ],
-      [
-        'logMaxSize',
-        undefined,
-        `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
-      ],
-      // performanceStorage section
-      ['URI', ConfigurationSection.performanceStorage, "Use 'uri' instead"],
-    ]
-    for (const [key, section, msg] of deprecatedKeys) {
-      Configuration.warnDeprecatedConfigurationKey(key, section, msg)
-    }
-    // station template url(s) remapping
-    if (
-      Configuration.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData] !=
-      null
-    ) {
-      const configurationData = Configuration.getConfigurationData()
-      if (configurationData != null) {
-        configurationData.stationTemplateUrls = configurationData[
-          'stationTemplateURLs' as keyof ConfigurationData
-        ] as StationTemplateUrl[]
-      }
-    }
-    Configuration.getConfigurationData()?.stationTemplateUrls.forEach(
-      (stationTemplateUrl: StationTemplateUrl) => {
-        if (stationTemplateUrl['numberOfStation' as keyof StationTemplateUrl] != null) {
-          console.error(
-            `${chalk.green(logPrefix())} ${chalk.red(
-              `Deprecated configuration key 'numberOfStation' usage for template file '${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use 'numberOfStations' instead`
-            )}`
-          )
-        }
-      }
-    )
-    // worker section: staticPool check
-    if (
-      Configuration.getConfigurationData()?.worker?.processType ===
-      ('staticPool' as WorkerProcessType)
-    ) {
-      console.error(
-        `${chalk.green(logPrefix())} ${chalk.red(
-          `Deprecated configuration 'staticPool' value usage in worker section 'processType' field. Use '${WorkerProcessType.fixedPool}' value instead`
-        )}`
-      )
-    }
-    // uiServer section
-    if (has('uiWebSocketServer', Configuration.getConfigurationData())) {
-      console.error(
-        `${chalk.green(logPrefix())} ${chalk.red(
-          `Deprecated configuration section 'uiWebSocketServer' usage. Use '${ConfigurationSection.uiServer}' instead`
-        )}`
-      )
-    }
-  }
-
   private static getConfigurationFileWatcher (): FSWatcher | undefined {
     if (
       Configuration.configurationFile == null ||
@@ -573,37 +403,4 @@ export class Configuration {
   private static isConfigurationSectionCached (sectionName: ConfigurationSection): boolean {
     return Configuration.configurationSectionCache.has(sectionName)
   }
-
-  private static warnDeprecatedConfigurationKey (
-    key: string,
-    configurationSection?: ConfigurationSection,
-    logMsgToAppend = ''
-  ): void {
-    if (
-      configurationSection != null &&
-      Configuration.getConfigurationData()?.[configurationSection as keyof ConfigurationData] !=
-        null &&
-      (
-        Configuration.getConfigurationData()?.[
-          configurationSection as keyof ConfigurationData
-        ] as Record<string, unknown>
-      )[key] != null
-    ) {
-      console.error(
-        `${chalk.green(logPrefix())} ${chalk.red(
-          `Deprecated configuration key '${key}' usage in section '${configurationSection}'${
-            logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
-          }`
-        )}`
-      )
-    } else if (Configuration.getConfigurationData()?.[key as keyof ConfigurationData] != null) {
-      console.error(
-        `${chalk.green(logPrefix())} ${chalk.red(
-          `Deprecated configuration key '${key}' usage${
-            logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
-          }`
-        )}`
-      )
-    }
-  }
 }
diff --git a/src/utils/ConfigurationMigration.ts b/src/utils/ConfigurationMigration.ts
new file mode 100644 (file)
index 0000000..65f34d3
--- /dev/null
@@ -0,0 +1,214 @@
+import chalk from 'chalk'
+
+import {
+  type ConfigurationData,
+  ConfigurationSection,
+  type StationTemplateUrl,
+} from '../types/index.js'
+import { WorkerProcessType } from '../worker/index.js'
+import { logPrefix } from './ConfigurationUtils.js'
+import { has } from './Utils.js'
+
+/**
+ * Check and warn about deprecated configuration keys
+ * @param configurationData - The configuration data to check
+ */
+export function checkDeprecatedConfigurationKeys (
+  configurationData: ConfigurationData | undefined
+): void {
+  const deprecatedKeys: [string, ConfigurationSection | undefined, string][] = [
+    // connection timeout
+    [
+      'autoReconnectTimeout',
+      undefined,
+      "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
+    ],
+    [
+      'connectionTimeout',
+      undefined,
+      "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
+    ],
+    // connection retries
+    ['autoReconnectMaxRetries', undefined, 'Use it in charging station template instead'],
+    // station template url(s)
+    ['stationTemplateURLs', undefined, "Use 'stationTemplateUrls' instead"],
+    // supervision url(s)
+    ['supervisionURLs', undefined, "Use 'supervisionUrls' instead"],
+    // supervision urls distribution
+    ['distributeStationToTenantEqually', undefined, "Use 'supervisionUrlDistribution' instead"],
+    ['distributeStationsToTenantsEqually', undefined, "Use 'supervisionUrlDistribution' instead"],
+    // worker section
+    [
+      'useWorkerPool',
+      undefined,
+      `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
+    ],
+    [
+      'workerProcess',
+      undefined,
+      `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
+    ],
+    [
+      'workerStartDelay',
+      undefined,
+      `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`,
+    ],
+    [
+      'chargingStationsPerWorker',
+      undefined,
+      `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`,
+    ],
+    [
+      'elementAddDelay',
+      undefined,
+      `Use '${ConfigurationSection.worker}' section to define the worker's element add delay instead`,
+    ],
+    [
+      'workerPoolMinSize',
+      undefined,
+      `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`,
+    ],
+    [
+      'workerPoolSize',
+      undefined,
+      `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
+    ],
+    [
+      'workerPoolMaxSize',
+      undefined,
+      `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
+    ],
+    [
+      'workerPoolStrategy',
+      undefined,
+      `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`,
+    ],
+    ['poolStrategy', ConfigurationSection.worker, 'Not publicly exposed to end users'],
+    ['elementStartDelay', ConfigurationSection.worker, "Use 'elementAddDelay' instead"],
+    // log section
+    [
+      'logEnabled',
+      undefined,
+      `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
+    ],
+    [
+      'logFile',
+      undefined,
+      `Use '${ConfigurationSection.log}' section to define the log file instead`,
+    ],
+    [
+      'logErrorFile',
+      undefined,
+      `Use '${ConfigurationSection.log}' section to define the log error file instead`,
+    ],
+    [
+      'logConsole',
+      undefined,
+      `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
+    ],
+    [
+      'logStatisticsInterval',
+      undefined,
+      `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
+    ],
+    [
+      'logLevel',
+      undefined,
+      `Use '${ConfigurationSection.log}' section to define the log level instead`,
+    ],
+    [
+      'logFormat',
+      undefined,
+      `Use '${ConfigurationSection.log}' section to define the log format instead`,
+    ],
+    [
+      'logRotate',
+      undefined,
+      `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
+    ],
+    [
+      'logMaxFiles',
+      undefined,
+      `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
+    ],
+    [
+      'logMaxSize',
+      undefined,
+      `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
+    ],
+    // performanceStorage section
+    ['URI', ConfigurationSection.performanceStorage, "Use 'uri' instead"],
+  ]
+  for (const [key, section, msg] of deprecatedKeys) {
+    warnDeprecatedConfigurationKey(configurationData, key, section, msg)
+  }
+  // station template url(s) remapping
+  if (configurationData?.['stationTemplateURLs' as keyof ConfigurationData] != null) {
+    configurationData.stationTemplateUrls = configurationData[
+      'stationTemplateURLs' as keyof ConfigurationData
+    ] as StationTemplateUrl[]
+  }
+  configurationData?.stationTemplateUrls.forEach((stationTemplateUrl: StationTemplateUrl) => {
+    if (stationTemplateUrl['numberOfStation' as keyof StationTemplateUrl] != null) {
+      console.error(
+        `${chalk.green(logPrefix())} ${chalk.red(
+          `Deprecated configuration key 'numberOfStation' usage for template file '${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use 'numberOfStations' instead`
+        )}`
+      )
+    }
+  })
+  // worker section: staticPool check
+  if (configurationData?.worker?.processType === ('staticPool' as WorkerProcessType)) {
+    console.error(
+      `${chalk.green(logPrefix())} ${chalk.red(
+        `Deprecated configuration 'staticPool' value usage in worker section 'processType' field. Use '${WorkerProcessType.fixedPool}' value instead`
+      )}`
+    )
+  }
+  // uiServer section
+  if (has('uiWebSocketServer', configurationData)) {
+    console.error(
+      `${chalk.green(logPrefix())} ${chalk.red(
+        `Deprecated configuration section 'uiWebSocketServer' usage. Use '${ConfigurationSection.uiServer}' instead`
+      )}`
+    )
+  }
+}
+
+/**
+ * Warn about a deprecated configuration key
+ * @param configurationData - The configuration data to check
+ * @param key - The deprecated key name
+ * @param configurationSection - The configuration section containing the key
+ * @param logMsgToAppend - Additional message to append to the warning
+ */
+function warnDeprecatedConfigurationKey (
+  configurationData: ConfigurationData | undefined,
+  key: string,
+  configurationSection?: ConfigurationSection,
+  logMsgToAppend = ''
+): void {
+  if (
+    configurationSection != null &&
+    configurationData?.[configurationSection as keyof ConfigurationData] != null &&
+    (configurationData[configurationSection as keyof ConfigurationData] as Record<string, unknown>)[
+      key
+    ] != null
+  ) {
+    console.error(
+      `${chalk.green(logPrefix())} ${chalk.red(
+        `Deprecated configuration key '${key}' usage in section '${configurationSection}'${
+          logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
+        }`
+      )}`
+    )
+  } else if (configurationData?.[key as keyof ConfigurationData] != null) {
+    console.error(
+      `${chalk.green(logPrefix())} ${chalk.red(
+        `Deprecated configuration key '${key}' usage${
+          logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
+        }`
+      )}`
+    )
+  }
+}