refactor: align set information namespace
[e-mobility-charging-stations-simulator.git] / src / utils / Configuration.ts
CommitLineData
130783a7
JB
1import fs from 'node:fs';
2import path from 'node:path';
3import { fileURLToPath } from 'node:url';
8114d10e
JB
4
5import chalk from 'chalk';
088ee3c1 6import merge from 'just-merge';
156cab2c 7import { WorkerChoiceStrategies } from 'poolifier';
8114d10e 8
878e026c 9import { Constants } from './Constants';
878e026c 10import { Utils } from './Utils';
83e00df1 11import {
268a74bb 12 ApplicationProtocol,
83e00df1 13 type ConfigurationData,
268a74bb 14 FileType,
83e00df1
JB
15 type StationTemplateUrl,
16 type StorageConfiguration,
268a74bb 17 StorageType,
e7aeea18 18 SupervisionUrlDistribution,
83e00df1
JB
19 type UIServerConfiguration,
20 type WorkerConfiguration,
268a74bb
JB
21} from '../types';
22import { WorkerConstants, WorkerProcessType } from '../worker';
7dde0b73 23
268a74bb 24export class Configuration {
a95873d8 25 private static configurationFile = path.join(
51022aa0 26 path.dirname(fileURLToPath(import.meta.url)),
e7aeea18
JB
27 'assets',
28 'config.json'
29 );
10068088 30
1895299d 31 private static configurationFileWatcher: fs.FSWatcher | undefined;
6e0964c8 32 private static configuration: ConfigurationData | null = null;
e57acf6a
JB
33 private static configurationChangeCallback: () => Promise<void>;
34
d5bd1c00
JB
35 private constructor() {
36 // This is intentional
37 }
38
aa7d6d95 39 public static setConfigurationChangeCallback(cb: () => Promise<void>): void {
e57acf6a
JB
40 Configuration.configurationChangeCallback = cb;
41 }
7dde0b73 42
aa7d6d95 43 public static getLogStatisticsInterval(): number | undefined {
e7aeea18
JB
44 Configuration.warnDeprecatedConfigurationKey(
45 'statisticsDisplayInterval',
1895299d 46 undefined,
e7aeea18
JB
47 "Use 'logStatisticsInterval' instead"
48 );
7dde0b73 49 // Read conf
ef4932d8 50 return Utils.hasOwnProp(Configuration.getConfig(), 'logStatisticsInterval')
1895299d 51 ? Configuration.getConfig()?.logStatisticsInterval
25f5a959 52 : Constants.DEFAULT_LOG_STATISTICS_INTERVAL;
72f041bd
JB
53 }
54
aa7d6d95 55 public static getUIServer(): UIServerConfiguration {
ef4932d8 56 if (Utils.hasOwnProp(Configuration.getConfig(), 'uiWebSocketServer')) {
66271092 57 console.error(
c5e52a07
JB
58 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
59 "Deprecated configuration section 'uiWebSocketServer' usage. Use 'uiServer' instead"
60 )}`
66271092
JB
61 );
62 }
675fa8e3 63 let uiServerConfiguration: UIServerConfiguration = {
b803eefa 64 enabled: false,
1f7fa4de 65 type: ApplicationProtocol.WS,
c127bd64 66 options: {
adbddcb4
JB
67 host: Constants.DEFAULT_UI_SERVER_HOST,
68 port: Constants.DEFAULT_UI_SERVER_PORT,
c127bd64 69 },
6a49ad23 70 };
ef4932d8 71 if (Utils.hasOwnProp(Configuration.getConfig(), 'uiServer')) {
598c886d
JB
72 uiServerConfiguration = merge<UIServerConfiguration>(
73 uiServerConfiguration,
74 Configuration.getConfig()?.uiServer
75 );
6a49ad23 76 }
dada83ec 77 if (Utils.isCFEnvironment() === true) {
72092cfc 78 delete uiServerConfiguration.options?.host;
b803eefa
JB
79 uiServerConfiguration.options.port = parseInt(process.env.PORT);
80 }
675fa8e3 81 return uiServerConfiguration;
6a49ad23
JB
82 }
83
aa7d6d95 84 public static getPerformanceStorage(): StorageConfiguration {
e7aeea18 85 Configuration.warnDeprecatedConfigurationKey('URI', 'performanceStorage', "Use 'uri' instead");
6a49ad23
JB
86 let storageConfiguration: StorageConfiguration = {
87 enabled: false,
88 type: StorageType.JSON_FILE,
e7aeea18 89 uri: this.getDefaultPerformanceStorageUri(StorageType.JSON_FILE),
6a49ad23 90 };
ef4932d8 91 if (Utils.hasOwnProp(Configuration.getConfig(), 'performanceStorage')) {
e7aeea18 92 storageConfiguration = {
1ba1e8fb 93 ...storageConfiguration,
1895299d 94 ...Configuration.getConfig()?.performanceStorage,
f682b2dc
JB
95 ...(Configuration.getConfig()?.performanceStorage?.type === StorageType.JSON_FILE &&
96 Configuration.getConfig()?.performanceStorage?.uri && {
e8044a69 97 uri: Configuration.buildPerformanceUriFilePath(
f682b2dc 98 new URL(Configuration.getConfig()?.performanceStorage?.uri).pathname
e8044a69 99 ),
f682b2dc 100 }),
72f041bd 101 };
72f041bd
JB
102 }
103 return storageConfiguration;
7dde0b73
JB
104 }
105
aa7d6d95 106 public static getAutoReconnectMaxRetries(): number | undefined {
e7aeea18
JB
107 Configuration.warnDeprecatedConfigurationKey(
108 'autoReconnectTimeout',
1895299d 109 undefined,
e7aeea18
JB
110 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
111 );
112 Configuration.warnDeprecatedConfigurationKey(
113 'connectionTimeout',
1895299d 114 undefined,
e7aeea18
JB
115 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
116 );
117 Configuration.warnDeprecatedConfigurationKey(
118 'autoReconnectMaxRetries',
1895299d 119 undefined,
e7aeea18
JB
120 'Use it in charging station template instead'
121 );
7dde0b73 122 // Read conf
ef4932d8 123 if (Utils.hasOwnProp(Configuration.getConfig(), 'autoReconnectMaxRetries')) {
1895299d 124 return Configuration.getConfig()?.autoReconnectMaxRetries;
3574dfd3 125 }
7dde0b73
JB
126 }
127
aa7d6d95 128 public static getStationTemplateUrls(): StationTemplateUrl[] | undefined {
e7aeea18
JB
129 Configuration.warnDeprecatedConfigurationKey(
130 'stationTemplateURLs',
1895299d 131 undefined,
e7aeea18
JB
132 "Use 'stationTemplateUrls' instead"
133 );
dada83ec 134 !Utils.isUndefined(Configuration.getConfig()['stationTemplateURLs']) &&
e7aeea18
JB
135 (Configuration.getConfig().stationTemplateUrls = Configuration.getConfig()[
136 'stationTemplateURLs'
617cad0c 137 ] as StationTemplateUrl[]);
7436ee0d
JB
138 Configuration.getConfig().stationTemplateUrls.forEach(
139 (stationTemplateUrl: StationTemplateUrl) => {
140 if (!Utils.isUndefined(stationTemplateUrl['numberOfStation'])) {
141 console.error(
c5e52a07
JB
142 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
143 `Deprecated configuration key 'numberOfStation' usage for template file '${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use 'numberOfStations' instead`
144 )}`
7436ee0d
JB
145 );
146 }
eb3937cb 147 }
7436ee0d 148 );
7dde0b73 149 // Read conf
1895299d 150 return Configuration.getConfig()?.stationTemplateUrls;
7dde0b73
JB
151 }
152
aa7d6d95 153 public static getWorker(): WorkerConfiguration {
e7aeea18 154 Configuration.warnDeprecatedConfigurationKey(
e80bc579 155 'useWorkerPool',
1895299d 156 undefined,
cf2a5d9b
JB
157 "Use 'worker' section to define the type of worker process model instead"
158 );
159 Configuration.warnDeprecatedConfigurationKey(
160 'workerProcess',
1895299d 161 undefined,
cf2a5d9b
JB
162 "Use 'worker' section to define the type of worker process model instead"
163 );
164 Configuration.warnDeprecatedConfigurationKey(
165 'workerStartDelay',
1895299d 166 undefined,
cf2a5d9b
JB
167 "Use 'worker' section to define the worker start delay instead"
168 );
169 Configuration.warnDeprecatedConfigurationKey(
170 'chargingStationsPerWorker',
1895299d 171 undefined,
cf2a5d9b
JB
172 "Use 'worker' section to define the number of element(s) per worker instead"
173 );
174 Configuration.warnDeprecatedConfigurationKey(
175 'elementStartDelay',
1895299d 176 undefined,
cf2a5d9b
JB
177 "Use 'worker' section to define the worker's element start delay instead"
178 );
179 Configuration.warnDeprecatedConfigurationKey(
180 'workerPoolMinSize',
1895299d 181 undefined,
cf2a5d9b 182 "Use 'worker' section to define the worker pool minimum size instead"
e7aeea18 183 );
e7aeea18
JB
184 Configuration.warnDeprecatedConfigurationKey(
185 'workerPoolSize;',
1895299d 186 undefined,
cf2a5d9b 187 "Use 'worker' section to define the worker pool maximum size instead"
e7aeea18 188 );
cf2a5d9b
JB
189 Configuration.warnDeprecatedConfigurationKey(
190 'workerPoolMaxSize;',
1895299d 191 undefined,
cf2a5d9b
JB
192 "Use 'worker' section to define the worker pool maximum size instead"
193 );
194 Configuration.warnDeprecatedConfigurationKey(
195 'workerPoolStrategy;',
1895299d 196 undefined,
cf2a5d9b
JB
197 "Use 'worker' section to define the worker pool strategy instead"
198 );
c127bd64 199 let workerConfiguration: WorkerConfiguration = {
ef4932d8 200 processType: Utils.hasOwnProp(Configuration.getConfig(), 'workerProcess')
1895299d 201 ? Configuration.getConfig()?.workerProcess
721646e9 202 : WorkerProcessType.workerSet,
ef4932d8 203 startDelay: Utils.hasOwnProp(Configuration.getConfig(), 'workerStartDelay')
1895299d 204 ? Configuration.getConfig()?.workerStartDelay
cf2a5d9b 205 : WorkerConstants.DEFAULT_WORKER_START_DELAY,
ef4932d8 206 elementsPerWorker: Utils.hasOwnProp(Configuration.getConfig(), 'chargingStationsPerWorker')
1895299d 207 ? Configuration.getConfig()?.chargingStationsPerWorker
cf2a5d9b 208 : WorkerConstants.DEFAULT_ELEMENTS_PER_WORKER,
ef4932d8 209 elementStartDelay: Utils.hasOwnProp(Configuration.getConfig(), 'elementStartDelay')
1895299d 210 ? Configuration.getConfig()?.elementStartDelay
cf2a5d9b 211 : WorkerConstants.DEFAULT_ELEMENT_START_DELAY,
ef4932d8 212 poolMinSize: Utils.hasOwnProp(Configuration.getConfig(), 'workerPoolMinSize')
1895299d 213 ? Configuration.getConfig()?.workerPoolMinSize
cf2a5d9b 214 : WorkerConstants.DEFAULT_POOL_MIN_SIZE,
ef4932d8 215 poolMaxSize: Utils.hasOwnProp(Configuration.getConfig(), 'workerPoolMaxSize')
1895299d 216 ? Configuration.getConfig()?.workerPoolMaxSize
cf2a5d9b 217 : WorkerConstants.DEFAULT_POOL_MAX_SIZE,
156cab2c 218 poolStrategy:
1895299d 219 Configuration.getConfig()?.workerPoolStrategy ?? WorkerChoiceStrategies.ROUND_ROBIN,
cf2a5d9b 220 };
ef4932d8 221 if (Utils.hasOwnProp(Configuration.getConfig(), 'worker')) {
1895299d 222 workerConfiguration = { ...workerConfiguration, ...Configuration.getConfig()?.worker };
cf2a5d9b
JB
223 }
224 return workerConfiguration;
3d2ff9e4
J
225 }
226
aa7d6d95
JB
227 public static workerPoolInUse(): boolean {
228 return [WorkerProcessType.dynamicPool, WorkerProcessType.staticPool].includes(
229 Configuration.getWorker().processType
230 );
231 }
232
233 public static workerDynamicPoolInUse(): boolean {
234 return Configuration.getWorker().processType === WorkerProcessType.dynamicPool;
235 }
236
ae389044
JB
237 public static getLogEnabled(): boolean | undefined {
238 return Utils.hasOwnProp(Configuration.getConfig(), 'logEnabled')
239 ? Configuration.getConfig()?.logEnabled
240 : true;
241 }
242
aa7d6d95 243 public static getLogConsole(): boolean | undefined {
1895299d
JB
244 Configuration.warnDeprecatedConfigurationKey(
245 'consoleLog',
246 undefined,
247 "Use 'logConsole' instead"
248 );
ef4932d8 249 return Utils.hasOwnProp(Configuration.getConfig(), 'logConsole')
1895299d 250 ? Configuration.getConfig()?.logConsole
e7aeea18 251 : false;
7dde0b73
JB
252 }
253
aa7d6d95 254 public static getLogFormat(): string | undefined {
ef4932d8 255 return Utils.hasOwnProp(Configuration.getConfig(), 'logFormat')
1895299d 256 ? Configuration.getConfig()?.logFormat
e7aeea18 257 : 'simple';
027b409a
JB
258 }
259
aa7d6d95 260 public static getLogRotate(): boolean | undefined {
ef4932d8 261 return Utils.hasOwnProp(Configuration.getConfig(), 'logRotate')
1895299d 262 ? Configuration.getConfig()?.logRotate
e7aeea18 263 : true;
6bf6769e
JB
264 }
265
aa7d6d95 266 public static getLogMaxFiles(): number | string | false | undefined {
9988696d 267 return (
ef4932d8 268 Utils.hasOwnProp(Configuration.getConfig(), 'logMaxFiles') &&
1895299d 269 Configuration.getConfig()?.logMaxFiles
9988696d
JB
270 );
271 }
272
aa7d6d95 273 public static getLogMaxSize(): number | string | false | undefined {
9988696d 274 return (
ef4932d8 275 Utils.hasOwnProp(Configuration.getConfig(), 'logMaxFiles') &&
1895299d 276 Configuration.getConfig()?.logMaxSize
9988696d 277 );
6bf6769e
JB
278 }
279
aa7d6d95 280 public static getLogLevel(): string | undefined {
ef4932d8 281 return Utils.hasOwnProp(Configuration.getConfig(), 'logLevel')
1895299d 282 ? Configuration.getConfig()?.logLevel?.toLowerCase()
e7aeea18 283 : 'info';
2e6f5966
JB
284 }
285
aa7d6d95 286 public static getLogFile(): string | undefined {
ef4932d8 287 return Utils.hasOwnProp(Configuration.getConfig(), 'logFile')
1895299d 288 ? Configuration.getConfig()?.logFile
e7aeea18 289 : 'combined.log';
7dde0b73
JB
290 }
291
aa7d6d95 292 public static getLogErrorFile(): string | undefined {
1895299d
JB
293 Configuration.warnDeprecatedConfigurationKey(
294 'errorFile',
295 undefined,
296 "Use 'logErrorFile' instead"
297 );
ef4932d8 298 return Utils.hasOwnProp(Configuration.getConfig(), 'logErrorFile')
1895299d 299 ? Configuration.getConfig()?.logErrorFile
e7aeea18 300 : 'error.log';
7dde0b73
JB
301 }
302
aa7d6d95 303 public static getSupervisionUrls(): string | string[] | undefined {
e7aeea18
JB
304 Configuration.warnDeprecatedConfigurationKey(
305 'supervisionURLs',
1895299d 306 undefined,
e7aeea18
JB
307 "Use 'supervisionUrls' instead"
308 );
dada83ec 309 !Utils.isUndefined(Configuration.getConfig()['supervisionURLs']) &&
1895299d
JB
310 (Configuration.getConfig().supervisionUrls = Configuration.getConfig()['supervisionURLs'] as
311 | string
312 | string[]);
7dde0b73 313 // Read conf
1895299d 314 return Configuration.getConfig()?.supervisionUrls;
7dde0b73
JB
315 }
316
aa7d6d95 317 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution | undefined {
e7aeea18
JB
318 Configuration.warnDeprecatedConfigurationKey(
319 'distributeStationToTenantEqually',
1895299d 320 undefined,
e7aeea18
JB
321 "Use 'supervisionUrlDistribution' instead"
322 );
323 Configuration.warnDeprecatedConfigurationKey(
324 'distributeStationsToTenantsEqually',
1895299d 325 undefined,
e7aeea18
JB
326 "Use 'supervisionUrlDistribution' instead"
327 );
ef4932d8 328 return Utils.hasOwnProp(Configuration.getConfig(), 'supervisionUrlDistribution')
1895299d 329 ? Configuration.getConfig()?.supervisionUrlDistribution
e7aeea18 330 : SupervisionUrlDistribution.ROUND_ROBIN;
7dde0b73 331 }
eb3937cb 332
8b7072dc 333 private static logPrefix = (): string => {
14ecae6a 334 return `${new Date().toLocaleString()} Simulator configuration |`;
8b7072dc 335 };
23132a44 336
e7aeea18
JB
337 private static warnDeprecatedConfigurationKey(
338 key: string,
339 sectionName?: string,
340 logMsgToAppend = ''
341 ) {
e7aeea18
JB
342 if (
343 sectionName &&
dada83ec
JB
344 !Utils.isUndefined(Configuration.getConfig()[sectionName]) &&
345 !Utils.isUndefined((Configuration.getConfig()[sectionName] as Record<string, unknown>)[key])
e7aeea18
JB
346 ) {
347 console.error(
c5e52a07
JB
348 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
349 `Deprecated configuration key '${key}' usage in section '${sectionName}'${
350 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
351 }`
352 )}`
e7aeea18 353 );
dada83ec 354 } else if (!Utils.isUndefined(Configuration.getConfig()[key])) {
e7aeea18 355 console.error(
c5e52a07
JB
356 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
357 `Deprecated configuration key '${key}' usage${
358 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
359 }`
360 )}`
e7aeea18 361 );
eb3937cb
JB
362 }
363 }
364
365 // Read the config file
1895299d 366 private static getConfig(): ConfigurationData | null {
eb3937cb 367 if (!Configuration.configuration) {
23132a44 368 try {
e7aeea18 369 Configuration.configuration = JSON.parse(
a95873d8 370 fs.readFileSync(Configuration.configurationFile, 'utf8')
e7aeea18 371 ) as ConfigurationData;
23132a44 372 } catch (error) {
69074173 373 Configuration.handleFileException(
a95873d8 374 Configuration.configurationFile,
7164966d
JB
375 FileType.Configuration,
376 error as NodeJS.ErrnoException,
69074173 377 Configuration.logPrefix()
e7aeea18 378 );
23132a44 379 }
ded13d97
JB
380 if (!Configuration.configurationFileWatcher) {
381 Configuration.configurationFileWatcher = Configuration.getConfigurationFileWatcher();
382 }
eb3937cb
JB
383 }
384 return Configuration.configuration;
385 }
963ee397 386
1895299d 387 private static getConfigurationFileWatcher(): fs.FSWatcher | undefined {
23132a44 388 try {
a95873d8 389 return fs.watch(Configuration.configurationFile, (event, filename): void => {
8d54dcc0 390 if (filename?.trim().length > 0 && event === 'change') {
3ec10737
JB
391 // Nullify to force configuration file reading
392 Configuration.configuration = null;
dada83ec 393 if (!Utils.isUndefined(Configuration.configurationChangeCallback)) {
72092cfc 394 Configuration.configurationChangeCallback().catch((error) => {
dcaf96dc
JB
395 throw typeof error === 'string' ? new Error(error) : error;
396 });
3ec10737 397 }
23132a44
JB
398 }
399 });
400 } catch (error) {
69074173 401 Configuration.handleFileException(
a95873d8 402 Configuration.configurationFile,
7164966d
JB
403 FileType.Configuration,
404 error as NodeJS.ErrnoException,
69074173 405 Configuration.logPrefix()
e7aeea18 406 );
23132a44 407 }
ded13d97
JB
408 }
409
69074173
JB
410 private static handleFileException(
411 file: string,
412 fileType: FileType,
413 error: NodeJS.ErrnoException,
414 logPrefix: string
415 ): void {
416 const prefix = Utils.isNotEmptyString(logPrefix) ? `${logPrefix} ` : '';
417 let logMsg: string;
418 switch (error.code) {
419 case 'ENOENT':
420 logMsg = `${fileType} file ${file} not found:`;
421 break;
422 case 'EEXIST':
423 logMsg = `${fileType} file ${file} already exists:`;
424 break;
425 case 'EACCES':
426 logMsg = `${fileType} file ${file} access denied:`;
427 break;
7b5dbe91
JB
428 case 'EPERM':
429 logMsg = `${fileType} file ${file} permission denied:`;
430 break;
69074173
JB
431 default:
432 logMsg = `${fileType} file ${file} error:`;
433 }
7b5dbe91
JB
434 console.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
435 throw error;
69074173
JB
436 }
437
1f5df42a 438 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
d5603918
JB
439 switch (storageType) {
440 case StorageType.JSON_FILE:
e8044a69 441 return Configuration.buildPerformanceUriFilePath(
e7aeea18 442 Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME
e8044a69 443 );
d5603918 444 case StorageType.SQLITE:
e8044a69 445 return Configuration.buildPerformanceUriFilePath(
5bcb75d4 446 `${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`
e8044a69 447 );
d5603918
JB
448 default:
449 throw new Error(`Performance storage URI is mandatory with storage type '${storageType}'`);
450 }
451 }
e8044a69
JB
452
453 private static buildPerformanceUriFilePath(file: string) {
454 return `file://${path.join(
51022aa0 455 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
e8044a69
JB
456 file
457 )}`;
458 }
7dde0b73 459}