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