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