1 import { type FSWatcher
, readFileSync
, watch
} from
'node:fs';
2 import { dirname
, join
, resolve
} from
'node:path';
3 import { fileURLToPath
} from
'node:url';
5 import chalk from
'chalk';
6 import merge from
'just-merge';
8 import { Constants
} from
'./Constants';
9 import { hasOwnProp
, isCFEnvironment
, isNotEmptyString
, isUndefined
} from
'./Utils';
12 type ConfigurationData
,
15 type LogConfiguration
,
16 type StationTemplateUrl
,
17 type StorageConfiguration
,
19 SupervisionUrlDistribution
,
20 type UIServerConfiguration
,
21 type WorkerConfiguration
,
23 import { WorkerConstants
, WorkerProcessType
} from
'../worker';
25 type ConfigurationSectionType
=
27 | StorageConfiguration
29 | UIServerConfiguration
;
31 export class Configuration
{
32 private static configurationFile
= join(
33 dirname(fileURLToPath(import.meta
.url
)),
38 private static configurationFileWatcher
: FSWatcher
| undefined;
39 private static configurationData
: ConfigurationData
| null = null;
40 private static configurationSectionCache
= new Map
<
42 ConfigurationSectionType
44 [ConfigurationSection
.log
, Configuration
.buildLogSection()],
45 [ConfigurationSection
.performanceStorage
, Configuration
.buildPerformanceStorageSection()],
46 [ConfigurationSection
.worker
, Configuration
.buildWorkerSection()],
47 [ConfigurationSection
.uiServer
, Configuration
.buildUIServerSection()],
50 private static configurationChangeCallback
: () => Promise
<void>;
52 private constructor() {
53 // This is intentional
56 public static setConfigurationChangeCallback(cb
: () => Promise
<void>): void {
57 Configuration
.configurationChangeCallback
= cb
;
60 public static getConfigurationSection
<T
extends ConfigurationSectionType
>(
61 sectionName
: ConfigurationSection
,
63 if (!Configuration
.configurationSectionCache
.has(sectionName
)) {
64 switch (sectionName
) {
65 case ConfigurationSection
.log
:
66 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildLogSection());
68 case ConfigurationSection
.performanceStorage
:
69 Configuration
.configurationSectionCache
.set(
71 Configuration
.buildPerformanceStorageSection(),
74 case ConfigurationSection
.worker
:
75 Configuration
.configurationSectionCache
.set(
77 Configuration
.buildWorkerSection(),
80 case ConfigurationSection
.uiServer
:
81 Configuration
.configurationSectionCache
.set(
83 Configuration
.buildUIServerSection(),
87 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
88 throw new Error(`Unknown configuration section '${sectionName}'`);
91 return Configuration
.configurationSectionCache
.get(sectionName
) as T
;
94 public static getAutoReconnectMaxRetries(): number | undefined {
95 Configuration
.warnDeprecatedConfigurationKey(
96 'autoReconnectTimeout',
98 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
100 Configuration
.warnDeprecatedConfigurationKey(
103 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
105 Configuration
.warnDeprecatedConfigurationKey(
106 'autoReconnectMaxRetries',
108 'Use it in charging station template instead',
110 if (hasOwnProp(Configuration
.getConfigurationData(), 'autoReconnectMaxRetries')) {
111 return Configuration
.getConfigurationData()?.autoReconnectMaxRetries
;
115 public static getStationTemplateUrls(): StationTemplateUrl
[] | undefined {
116 Configuration
.warnDeprecatedConfigurationKey(
117 'stationTemplateURLs',
119 "Use 'stationTemplateUrls' instead",
121 // eslint-disable-next-line @typescript-eslint/dot-notation
122 !isUndefined(Configuration
.getConfigurationData()!['stationTemplateURLs']) &&
123 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
124 Configuration
.getConfigurationData()![
125 // eslint-disable-next-line @typescript-eslint/dot-notation
126 'stationTemplateURLs'
127 ] as StationTemplateUrl
[]);
128 Configuration
.getConfigurationData()!.stationTemplateUrls
.forEach(
129 (stationTemplateUrl
: StationTemplateUrl
) => {
130 // eslint-disable-next-line @typescript-eslint/dot-notation
131 if (!isUndefined(stationTemplateUrl
['numberOfStation'])) {
133 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
134 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`,
140 return Configuration
.getConfigurationData()?.stationTemplateUrls
;
143 public static getSupervisionUrls(): string | string[] | undefined {
144 Configuration
.warnDeprecatedConfigurationKey(
147 "Use 'supervisionUrls' instead",
149 // eslint-disable-next-line @typescript-eslint/dot-notation
150 if (!isUndefined(Configuration
.getConfigurationData()!['supervisionURLs'])) {
151 Configuration
.getConfigurationData()!.supervisionUrls
= Configuration
.getConfigurationData()![
152 // eslint-disable-next-line @typescript-eslint/dot-notation
154 ] as string | string[];
156 return Configuration
.getConfigurationData()?.supervisionUrls
;
159 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
160 Configuration
.warnDeprecatedConfigurationKey(
161 'distributeStationToTenantEqually',
163 "Use 'supervisionUrlDistribution' instead",
165 Configuration
.warnDeprecatedConfigurationKey(
166 'distributeStationsToTenantsEqually',
168 "Use 'supervisionUrlDistribution' instead",
170 return hasOwnProp(Configuration
.getConfigurationData(), 'supervisionUrlDistribution')
171 ? Configuration
.getConfigurationData()?.supervisionUrlDistribution
172 : SupervisionUrlDistribution
.ROUND_ROBIN
;
175 public static workerPoolInUse(): boolean {
176 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.staticPool
].includes(
177 Configuration
.buildWorkerSection().processType
!,
181 public static workerDynamicPoolInUse(): boolean {
182 return Configuration
.buildWorkerSection().processType
=== WorkerProcessType
.dynamicPool
;
185 private static buildUIServerSection(): UIServerConfiguration
{
186 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
188 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
189 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`,
193 let uiServerConfiguration
: UIServerConfiguration
= {
195 type: ApplicationProtocol
.WS
,
197 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
198 port
: Constants
.DEFAULT_UI_SERVER_PORT
,
201 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
202 uiServerConfiguration
= merge
<UIServerConfiguration
>(
203 uiServerConfiguration
,
204 Configuration
.getConfigurationData()!.uiServer
!,
207 if (isCFEnvironment() === true) {
208 delete uiServerConfiguration
.options
?.host
;
209 uiServerConfiguration
.options
!.port
= parseInt(process
.env
.PORT
!);
211 return uiServerConfiguration
;
214 private static buildPerformanceStorageSection(): StorageConfiguration
{
215 Configuration
.warnDeprecatedConfigurationKey(
217 ConfigurationSection
.performanceStorage
,
220 let storageConfiguration
: StorageConfiguration
= {
222 type: StorageType
.JSON_FILE
,
223 uri
: this.getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
225 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.performanceStorage
)) {
226 storageConfiguration
= {
227 ...storageConfiguration
,
228 ...Configuration
.getConfigurationData()?.performanceStorage
,
229 ...(Configuration
.getConfigurationData()?.performanceStorage
?.type ===
230 StorageType
.JSON_FILE
&&
231 Configuration
.getConfigurationData()?.performanceStorage
?.uri
&& {
232 uri
: Configuration
.buildPerformanceUriFilePath(
233 new URL(Configuration
.getConfigurationData()!.performanceStorage
!.uri
!).pathname
,
238 return storageConfiguration
;
241 private static buildLogSection(): LogConfiguration
{
242 Configuration
.warnDeprecatedConfigurationKey(
245 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
247 Configuration
.warnDeprecatedConfigurationKey(
250 `Use '${ConfigurationSection.log}' section to define the log file instead`,
252 Configuration
.warnDeprecatedConfigurationKey(
255 `Use '${ConfigurationSection.log}' section to define the log error file instead`,
257 Configuration
.warnDeprecatedConfigurationKey(
260 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
262 Configuration
.warnDeprecatedConfigurationKey(
263 'logStatisticsInterval',
265 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
267 Configuration
.warnDeprecatedConfigurationKey(
270 `Use '${ConfigurationSection.log}' section to define the log level instead`,
272 Configuration
.warnDeprecatedConfigurationKey(
275 `Use '${ConfigurationSection.log}' section to define the log format instead`,
277 Configuration
.warnDeprecatedConfigurationKey(
280 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
282 Configuration
.warnDeprecatedConfigurationKey(
285 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
287 Configuration
.warnDeprecatedConfigurationKey(
290 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
292 const defaultLogConfiguration
: LogConfiguration
= {
294 file
: 'logs/combined.log',
295 errorFile
: 'logs/error.log',
296 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
301 const deprecatedLogConfiguration
: LogConfiguration
= {
302 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logEnabled') && {
303 enabled
: Configuration
.getConfigurationData()?.logEnabled
,
305 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
306 file
: Configuration
.getConfigurationData()?.logFile
,
308 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
309 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
,
311 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
312 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
,
314 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
315 level
: Configuration
.getConfigurationData()?.logLevel
,
317 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
318 console
: Configuration
.getConfigurationData()?.logConsole
,
320 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
321 format
: Configuration
.getConfigurationData()?.logFormat
,
323 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
324 rotate
: Configuration
.getConfigurationData()?.logRotate
,
326 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
327 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
,
329 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
330 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
,
333 const logConfiguration
: LogConfiguration
= {
334 ...defaultLogConfiguration
,
335 ...deprecatedLogConfiguration
,
336 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
337 Configuration
.getConfigurationData()?.log
),
339 return logConfiguration
;
342 private static buildWorkerSection(): WorkerConfiguration
{
343 Configuration
.warnDeprecatedConfigurationKey(
346 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
348 Configuration
.warnDeprecatedConfigurationKey(
351 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
353 Configuration
.warnDeprecatedConfigurationKey(
356 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`,
358 Configuration
.warnDeprecatedConfigurationKey(
359 'chargingStationsPerWorker',
361 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`,
363 Configuration
.warnDeprecatedConfigurationKey(
366 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`,
368 Configuration
.warnDeprecatedConfigurationKey(
371 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`,
373 Configuration
.warnDeprecatedConfigurationKey(
376 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
378 Configuration
.warnDeprecatedConfigurationKey(
379 'workerPoolMaxSize;',
381 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
383 Configuration
.warnDeprecatedConfigurationKey(
384 'workerPoolStrategy;',
386 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`,
388 const defaultWorkerConfiguration
: WorkerConfiguration
= {
389 processType
: WorkerProcessType
.workerSet
,
390 startDelay
: WorkerConstants
.DEFAULT_WORKER_START_DELAY
,
391 elementsPerWorker
: 'auto',
392 elementStartDelay
: WorkerConstants
.DEFAULT_ELEMENT_START_DELAY
,
393 poolMinSize
: WorkerConstants
.DEFAULT_POOL_MIN_SIZE
,
394 poolMaxSize
: WorkerConstants
.DEFAULT_POOL_MAX_SIZE
,
396 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
397 delete Configuration
.getConfigurationData()?.workerPoolStrategy
;
398 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
399 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
400 processType
: Configuration
.getConfigurationData()?.workerProcess
,
402 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
403 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
,
405 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
406 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
,
408 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementStartDelay') && {
409 elementStartDelay
: Configuration
.getConfigurationData()?.elementStartDelay
,
411 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
412 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
,
414 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
415 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
,
418 Configuration
.warnDeprecatedConfigurationKey(
420 ConfigurationSection
.worker
,
421 'Not publicly exposed to end users',
423 const workerConfiguration
: WorkerConfiguration
= {
424 ...defaultWorkerConfiguration
,
425 ...deprecatedWorkerConfiguration
,
426 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
427 Configuration
.getConfigurationData()?.worker
),
429 if (!Object.values(WorkerProcessType
).includes(workerConfiguration
.processType
!)) {
430 throw new SyntaxError(
431 `Invalid worker process type '${workerConfiguration.processType}' defined in configuration`,
434 return workerConfiguration
;
437 private static logPrefix
= (): string => {
438 return `${new Date().toLocaleString()} Simulator configuration |`;
441 private static warnDeprecatedConfigurationKey(
443 sectionName
?: string,
448 !isUndefined(Configuration
.getConfigurationData()![sectionName
]) &&
450 (Configuration
.getConfigurationData()![sectionName
] as Record
<string, unknown
>)[key
],
454 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
455 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
456 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
460 } else if (!isUndefined(Configuration
.getConfigurationData()![key
])) {
462 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
463 `Deprecated configuration key
'${key}' usage$
{
464 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
471 private static getConfigurationData(): ConfigurationData
| null {
472 if (!Configuration
.configurationData
) {
474 Configuration
.configurationData
= JSON
.parse(
475 readFileSync(Configuration
.configurationFile
, 'utf8'),
476 ) as ConfigurationData
;
478 Configuration
.handleFileException(
479 Configuration
.configurationFile
,
480 FileType
.Configuration
,
481 error
as NodeJS
.ErrnoException
,
482 Configuration
.logPrefix(),
485 if (!Configuration
.configurationFileWatcher
) {
486 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
489 return Configuration
.configurationData
;
492 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
494 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
495 if (filename
!.trim()!.length
> 0 && event
=== 'change') {
496 // Nullify to force configuration file reading
497 Configuration
.configurationData
= null;
498 Configuration
.configurationSectionCache
.clear();
499 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
500 Configuration
.configurationChangeCallback().catch((error
) => {
501 throw typeof error
=== 'string' ? new Error(error
) : error
;
507 Configuration
.handleFileException(
508 Configuration
.configurationFile
,
509 FileType
.Configuration
,
510 error
as NodeJS
.ErrnoException
,
511 Configuration
.logPrefix(),
516 private static handleFileException(
519 error
: NodeJS
.ErrnoException
,
522 const prefix
= isNotEmptyString(logPrefix
) ? `${logPrefix} ` : '';
524 switch (error
.code
) {
526 logMsg
= `${fileType} file ${file} not found:`;
529 logMsg
= `${fileType} file ${file} already exists:`;
532 logMsg
= `${fileType} file ${file} access denied:`;
535 logMsg
= `${fileType} file ${file} permission denied:`;
538 logMsg
= `${fileType} file ${file} error:`;
540 console
.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
544 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
545 switch (storageType) {
546 case StorageType.JSON_FILE:
547 return Configuration.buildPerformanceUriFilePath(
548 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}
`,
550 case StorageType.SQLITE:
551 return Configuration.buildPerformanceUriFilePath(
552 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}
.db
`,
555 throw new Error(`Performance storage URI
is mandatory
with storage
type '${storageType}'`);
559 private static buildPerformanceUriFilePath(file: string) {
560 return `file
://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;