Update to poolifier 2.3.1
[e-mobility-charging-stations-simulator.git] / src / utils / Configuration.ts
CommitLineData
8114d10e
JB
1import fs from 'fs';
2import path from 'path';
3import { fileURLToPath } from 'url';
4
5import chalk from 'chalk';
156cab2c 6import { WorkerChoiceStrategies } from 'poolifier';
8114d10e 7
83e00df1
JB
8import {
9 type ConfigurationData,
10 type StationTemplateUrl,
11 type StorageConfiguration,
e7aeea18 12 SupervisionUrlDistribution,
83e00df1
JB
13 type UIServerConfiguration,
14 type WorkerConfiguration,
e7aeea18 15} from '../types/ConfigurationData';
6c1761d4
JB
16import type { EmptyObject } from '../types/EmptyObject';
17import type { HandleErrorParams } from '../types/Error';
8114d10e 18import { FileType } from '../types/FileType';
72f041bd 19import { StorageType } from '../types/Storage';
1f7fa4de 20import { ApplicationProtocol } from '../types/UIProtocol';
a4624c96 21import { WorkerProcessType } from '../types/Worker';
8114d10e
JB
22import WorkerConstants from '../worker/WorkerConstants';
23import Constants from './Constants';
7dde0b73 24
3f40bc9c 25export default 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
ded13d97 32 private static configurationFileWatcher: fs.FSWatcher;
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
72f041bd 44 static getLogStatisticsInterval(): number {
e7aeea18
JB
45 Configuration.warnDeprecatedConfigurationKey(
46 'statisticsDisplayInterval',
47 null,
48 "Use 'logStatisticsInterval' instead"
49 );
7dde0b73 50 // Read conf
e7aeea18
JB
51 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logStatisticsInterval')
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')) {
d5bd1c00
JB
71 uiServerConfiguration = Configuration.deepMerge(
72 uiServerConfiguration,
73 Configuration.getConfig().uiServer
74 );
6a49ad23 75 }
b803eefa
JB
76 if (Configuration.isCFEnvironment() === true) {
77 delete uiServerConfiguration.options.host;
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,
c127bd64 93 ...Configuration.getConfig().performanceStorage,
72f041bd 94 };
72f041bd
JB
95 }
96 return storageConfiguration;
7dde0b73
JB
97 }
98
9ccca265 99 static getAutoReconnectMaxRetries(): number {
e7aeea18
JB
100 Configuration.warnDeprecatedConfigurationKey(
101 'autoReconnectTimeout',
102 null,
103 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
104 );
105 Configuration.warnDeprecatedConfigurationKey(
106 'connectionTimeout',
107 null,
108 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
109 );
110 Configuration.warnDeprecatedConfigurationKey(
111 'autoReconnectMaxRetries',
112 null,
113 'Use it in charging station template instead'
114 );
7dde0b73 115 // Read conf
963ee397 116 if (Configuration.objectHasOwnProperty(Configuration.getConfig(), 'autoReconnectMaxRetries')) {
3574dfd3
JB
117 return Configuration.getConfig().autoReconnectMaxRetries;
118 }
7dde0b73
JB
119 }
120
1f5df42a 121 static getStationTemplateUrls(): StationTemplateUrl[] {
e7aeea18
JB
122 Configuration.warnDeprecatedConfigurationKey(
123 'stationTemplateURLs',
124 null,
125 "Use 'stationTemplateUrls' instead"
126 );
127 !Configuration.isUndefined(Configuration.getConfig()['stationTemplateURLs']) &&
128 (Configuration.getConfig().stationTemplateUrls = Configuration.getConfig()[
129 'stationTemplateURLs'
130 ] 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
1f5df42a 141 return Configuration.getConfig().stationTemplateUrls;
7dde0b73
JB
142 }
143
cf2a5d9b 144 static getWorker(): WorkerConfiguration {
e7aeea18 145 Configuration.warnDeprecatedConfigurationKey(
e80bc579 146 'useWorkerPool',
e7aeea18 147 null,
cf2a5d9b
JB
148 "Use 'worker' section to define the type of worker process model instead"
149 );
150 Configuration.warnDeprecatedConfigurationKey(
151 'workerProcess',
152 null,
153 "Use 'worker' section to define the type of worker process model instead"
154 );
155 Configuration.warnDeprecatedConfigurationKey(
156 'workerStartDelay',
157 null,
158 "Use 'worker' section to define the worker start delay instead"
159 );
160 Configuration.warnDeprecatedConfigurationKey(
161 'chargingStationsPerWorker',
162 null,
163 "Use 'worker' section to define the number of element(s) per worker instead"
164 );
165 Configuration.warnDeprecatedConfigurationKey(
166 'elementStartDelay',
167 null,
168 "Use 'worker' section to define the worker's element start delay instead"
169 );
170 Configuration.warnDeprecatedConfigurationKey(
171 'workerPoolMinSize',
172 null,
173 "Use 'worker' section to define the worker pool minimum size instead"
e7aeea18 174 );
e7aeea18
JB
175 Configuration.warnDeprecatedConfigurationKey(
176 'workerPoolSize;',
177 null,
cf2a5d9b 178 "Use 'worker' section to define the worker pool maximum size instead"
e7aeea18 179 );
cf2a5d9b
JB
180 Configuration.warnDeprecatedConfigurationKey(
181 'workerPoolMaxSize;',
182 null,
183 "Use 'worker' section to define the worker pool maximum size instead"
184 );
185 Configuration.warnDeprecatedConfigurationKey(
186 'workerPoolStrategy;',
187 null,
188 "Use 'worker' section to define the worker pool strategy instead"
189 );
c127bd64 190 let workerConfiguration: WorkerConfiguration = {
cf2a5d9b
JB
191 processType: Configuration.objectHasOwnProperty(Configuration.getConfig(), 'workerProcess')
192 ? Configuration.getConfig().workerProcess
193 : WorkerProcessType.WORKER_SET,
194 startDelay: Configuration.objectHasOwnProperty(Configuration.getConfig(), 'workerStartDelay')
195 ? Configuration.getConfig().workerStartDelay
196 : WorkerConstants.DEFAULT_WORKER_START_DELAY,
197 elementsPerWorker: Configuration.objectHasOwnProperty(
198 Configuration.getConfig(),
199 'chargingStationsPerWorker'
200 )
201 ? Configuration.getConfig().chargingStationsPerWorker
202 : WorkerConstants.DEFAULT_ELEMENTS_PER_WORKER,
203 elementStartDelay: Configuration.objectHasOwnProperty(
204 Configuration.getConfig(),
205 'elementStartDelay'
206 )
207 ? Configuration.getConfig().elementStartDelay
208 : WorkerConstants.DEFAULT_ELEMENT_START_DELAY,
209 poolMinSize: Configuration.objectHasOwnProperty(
210 Configuration.getConfig(),
211 'workerPoolMinSize'
212 )
213 ? Configuration.getConfig().workerPoolMinSize
214 : WorkerConstants.DEFAULT_POOL_MIN_SIZE,
215 poolMaxSize: Configuration.objectHasOwnProperty(
216 Configuration.getConfig(),
217 'workerPoolMaxSize'
218 )
219 ? Configuration.getConfig().workerPoolMaxSize
220 : WorkerConstants.DEFAULT_POOL_MAX_SIZE,
156cab2c
JB
221 poolStrategy:
222 Configuration.getConfig().workerPoolStrategy ?? WorkerChoiceStrategies.FAIR_SHARE,
cf2a5d9b
JB
223 };
224 if (Configuration.objectHasOwnProperty(Configuration.getConfig(), 'worker')) {
c127bd64 225 workerConfiguration = { ...workerConfiguration, ...Configuration.getConfig().worker };
cf2a5d9b
JB
226 }
227 return workerConfiguration;
3d2ff9e4
J
228 }
229
7ec46a9a 230 static getLogConsole(): boolean {
e7aeea18
JB
231 Configuration.warnDeprecatedConfigurationKey('consoleLog', null, "Use 'logConsole' instead");
232 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logConsole')
233 ? Configuration.getConfig().logConsole
234 : false;
7dde0b73
JB
235 }
236
a4a21709 237 static getLogFormat(): string {
e7aeea18
JB
238 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logFormat')
239 ? Configuration.getConfig().logFormat
240 : 'simple';
027b409a
JB
241 }
242
6bf6769e 243 static getLogRotate(): boolean {
e7aeea18
JB
244 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logRotate')
245 ? Configuration.getConfig().logRotate
246 : true;
6bf6769e
JB
247 }
248
249 static getLogMaxFiles(): number {
e7aeea18
JB
250 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logMaxFiles')
251 ? Configuration.getConfig().logMaxFiles
252 : 7;
6bf6769e
JB
253 }
254
324fd4ee 255 static getLogLevel(): string {
e7aeea18
JB
256 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logLevel')
257 ? Configuration.getConfig().logLevel.toLowerCase()
258 : 'info';
2e6f5966
JB
259 }
260
a4a21709 261 static getLogFile(): string {
e7aeea18
JB
262 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logFile')
263 ? Configuration.getConfig().logFile
264 : 'combined.log';
7dde0b73
JB
265 }
266
7ec46a9a 267 static getLogErrorFile(): string {
e7aeea18
JB
268 Configuration.warnDeprecatedConfigurationKey('errorFile', null, "Use 'logErrorFile' instead");
269 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logErrorFile')
270 ? Configuration.getConfig().logErrorFile
271 : 'error.log';
7dde0b73
JB
272 }
273
2dcfe98e 274 static getSupervisionUrls(): string | string[] {
e7aeea18
JB
275 Configuration.warnDeprecatedConfigurationKey(
276 'supervisionURLs',
277 null,
278 "Use 'supervisionUrls' instead"
279 );
280 !Configuration.isUndefined(Configuration.getConfig()['supervisionURLs']) &&
281 (Configuration.getConfig().supervisionUrls = Configuration.getConfig()[
282 'supervisionURLs'
283 ] as string[]);
7dde0b73 284 // Read conf
1f5df42a 285 return Configuration.getConfig().supervisionUrls;
7dde0b73
JB
286 }
287
2dcfe98e 288 static getSupervisionUrlDistribution(): SupervisionUrlDistribution {
e7aeea18
JB
289 Configuration.warnDeprecatedConfigurationKey(
290 'distributeStationToTenantEqually',
291 null,
292 "Use 'supervisionUrlDistribution' instead"
293 );
294 Configuration.warnDeprecatedConfigurationKey(
295 'distributeStationsToTenantsEqually',
296 null,
297 "Use 'supervisionUrlDistribution' instead"
298 );
299 return Configuration.objectHasOwnProperty(
300 Configuration.getConfig(),
301 'supervisionUrlDistribution'
302 )
303 ? Configuration.getConfig().supervisionUrlDistribution
304 : SupervisionUrlDistribution.ROUND_ROBIN;
7dde0b73 305 }
eb3937cb 306
23132a44
JB
307 private static logPrefix(): string {
308 return new Date().toLocaleString() + ' Simulator configuration |';
309 }
310
e7aeea18
JB
311 private static warnDeprecatedConfigurationKey(
312 key: string,
313 sectionName?: string,
314 logMsgToAppend = ''
315 ) {
e7aeea18
JB
316 if (
317 sectionName &&
318 !Configuration.isUndefined(Configuration.getConfig()[sectionName]) &&
455ee9cf
JB
319 !Configuration.isUndefined(
320 (Configuration.getConfig()[sectionName] as Record<string, unknown>)[key]
321 )
e7aeea18
JB
322 ) {
323 console.error(
324 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration key '${key}' usage in section '${sectionName}'${
325 logMsgToAppend && '. ' + logMsgToAppend
326 }}`
327 );
912136b1 328 } else if (!Configuration.isUndefined(Configuration.getConfig()[key])) {
e7aeea18
JB
329 console.error(
330 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration key '${key}' usage${
331 logMsgToAppend && '. ' + logMsgToAppend
332 }}`
333 );
eb3937cb
JB
334 }
335 }
336
337 // Read the config file
338 private static getConfig(): ConfigurationData {
339 if (!Configuration.configuration) {
23132a44 340 try {
e7aeea18 341 Configuration.configuration = JSON.parse(
a95873d8 342 fs.readFileSync(Configuration.configurationFile, 'utf8')
e7aeea18 343 ) as ConfigurationData;
23132a44 344 } catch (error) {
e7aeea18
JB
345 Configuration.handleFileException(
346 Configuration.logPrefix(),
a95873d8
JB
347 FileType.Configuration,
348 Configuration.configurationFile,
3fa0f0ed 349 error as NodeJS.ErrnoException
e7aeea18 350 );
23132a44 351 }
ded13d97
JB
352 if (!Configuration.configurationFileWatcher) {
353 Configuration.configurationFileWatcher = Configuration.getConfigurationFileWatcher();
354 }
eb3937cb
JB
355 }
356 return Configuration.configuration;
357 }
963ee397 358
ded13d97 359 private static getConfigurationFileWatcher(): fs.FSWatcher {
23132a44 360 try {
a95873d8 361 return fs.watch(Configuration.configurationFile, (event, filename): void => {
3ec10737
JB
362 if (filename && event === 'change') {
363 // Nullify to force configuration file reading
364 Configuration.configuration = null;
365 if (!Configuration.isUndefined(Configuration.configurationChangeCallback)) {
dcaf96dc
JB
366 Configuration.configurationChangeCallback().catch((error) => {
367 throw typeof error === 'string' ? new Error(error) : error;
368 });
3ec10737 369 }
23132a44
JB
370 }
371 });
372 } catch (error) {
e7aeea18
JB
373 Configuration.handleFileException(
374 Configuration.logPrefix(),
a95873d8
JB
375 FileType.Configuration,
376 Configuration.configurationFile,
377 error as NodeJS.ErrnoException
e7aeea18 378 );
23132a44 379 }
ded13d97
JB
380 }
381
b803eefa
JB
382 private static isCFEnvironment(): boolean {
383 return process.env.VCAP_APPLICATION !== undefined;
384 }
385
1f5df42a 386 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
d5603918
JB
387 const SQLiteFileName = `${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`;
388 switch (storageType) {
389 case StorageType.JSON_FILE:
e7aeea18 390 return `file://${path.join(
0d8140bd 391 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../'),
e7aeea18
JB
392 Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME
393 )}`;
d5603918 394 case StorageType.SQLITE:
0d8140bd
JB
395 return `file://${path.join(
396 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../'),
397 SQLiteFileName
398 )}`;
d5603918
JB
399 default:
400 throw new Error(`Performance storage URI is mandatory with storage type '${storageType}'`);
401 }
402 }
403
d5bd1c00 404 private static isObject(item): boolean {
53e5fd67 405 return item && typeof item === 'object' && Array.isArray(item) === false;
d5bd1c00
JB
406 }
407
b803eefa
JB
408 private static objectHasOwnProperty(object: unknown, property: string): boolean {
409 return Object.prototype.hasOwnProperty.call(object, property) as boolean;
410 }
411
412 private static isUndefined(obj: unknown): boolean {
413 return typeof obj === 'undefined';
414 }
415
d5bd1c00
JB
416 private static deepMerge(target: object, ...sources: object[]): object {
417 if (!sources.length) {
418 return target;
419 }
420 const source = sources.shift();
421
422 if (Configuration.isObject(target) && Configuration.isObject(source)) {
423 for (const key in source) {
424 if (Configuration.isObject(source[key])) {
425 if (!target[key]) {
426 Object.assign(target, { [key]: {} });
427 }
428 // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
429 Configuration.deepMerge(target[key], source[key]);
430 } else {
431 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
432 Object.assign(target, { [key]: source[key] });
433 }
434 }
435 }
436 return Configuration.deepMerge(target, ...sources);
437 }
438
e7aeea18
JB
439 private static handleFileException(
440 logPrefix: string,
a95873d8 441 fileType: FileType,
e7aeea18
JB
442 filePath: string,
443 error: NodeJS.ErrnoException,
444 params: HandleErrorParams<EmptyObject> = { throwError: true }
445 ): void {
23132a44
JB
446 const prefix = logPrefix.length !== 0 ? logPrefix + ' ' : '';
447 if (error.code === 'ENOENT') {
e7aeea18
JB
448 console.error(
449 chalk.green(prefix) + chalk.red(fileType + ' file ' + filePath + ' not found: '),
450 error
451 );
72f041bd 452 } else if (error.code === 'EEXIST') {
e7aeea18
JB
453 console.error(
454 chalk.green(prefix) + chalk.red(fileType + ' file ' + filePath + ' already exists: '),
455 error
456 );
72f041bd 457 } else if (error.code === 'EACCES') {
e7aeea18
JB
458 console.error(
459 chalk.green(prefix) + chalk.red(fileType + ' file ' + filePath + ' access denied: '),
460 error
461 );
23132a44 462 } else {
e7aeea18
JB
463 console.error(
464 chalk.green(prefix) + chalk.red(fileType + ' file ' + filePath + ' error: '),
465 error
466 );
23132a44 467 }
e0a50bcd
JB
468 if (params?.throwError) {
469 throw error;
470 }
23132a44 471 }
7dde0b73 472}