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