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