Refine TS and linter configuration
[e-mobility-charging-stations-simulator.git] / src / utils / Configuration.ts
1 import fs from 'fs';
2 import path from 'path';
3 import { fileURLToPath } from 'url';
4
5 import chalk from 'chalk';
6
7 import ConfigurationData, {
8 StationTemplateUrl,
9 StorageConfiguration,
10 SupervisionUrlDistribution,
11 UIServerConfiguration,
12 WorkerConfiguration,
13 } from '../types/ConfigurationData';
14 import type { EmptyObject } from '../types/EmptyObject';
15 import type { HandleErrorParams } from '../types/Error';
16 import { FileType } from '../types/FileType';
17 import { StorageType } from '../types/Storage';
18 import { ApplicationProtocol } from '../types/UIProtocol';
19 import { WorkerProcessType } from '../types/Worker';
20 import WorkerConstants from '../worker/WorkerConstants';
21 import Constants from './Constants';
22
23 export default class Configuration {
24 private static configurationFile = path.join(
25 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
26 'assets',
27 'config.json'
28 );
29
30 private static configurationFileWatcher: fs.FSWatcher;
31 private static configuration: ConfigurationData | null = null;
32 private static configurationChangeCallback: () => Promise<void>;
33
34 private constructor() {
35 // This is intentional
36 }
37
38 static setConfigurationChangeCallback(cb: () => Promise<void>): void {
39 Configuration.configurationChangeCallback = cb;
40 }
41
42 static getLogStatisticsInterval(): number {
43 Configuration.warnDeprecatedConfigurationKey(
44 'statisticsDisplayInterval',
45 null,
46 "Use 'logStatisticsInterval' instead"
47 );
48 // Read conf
49 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logStatisticsInterval')
50 ? Configuration.getConfig().logStatisticsInterval
51 : Constants.DEFAULT_LOG_STATISTICS_INTERVAL;
52 }
53
54 static getUIServer(): UIServerConfiguration {
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 }
60 let uiServerConfiguration: UIServerConfiguration = {
61 enabled: true,
62 type: ApplicationProtocol.WS,
63 options: {
64 host: Constants.DEFAULT_UI_WEBSOCKET_SERVER_HOST,
65 port: Constants.DEFAULT_UI_WEBSOCKET_SERVER_PORT,
66 },
67 };
68 if (Configuration.objectHasOwnProperty(Configuration.getConfig(), 'uiServer')) {
69 uiServerConfiguration = Configuration.deepMerge(
70 uiServerConfiguration,
71 Configuration.getConfig().uiServer
72 );
73 }
74 return uiServerConfiguration;
75 }
76
77 static getPerformanceStorage(): StorageConfiguration {
78 Configuration.warnDeprecatedConfigurationKey('URI', 'performanceStorage', "Use 'uri' instead");
79 let storageConfiguration: StorageConfiguration = {
80 enabled: false,
81 type: StorageType.JSON_FILE,
82 uri: this.getDefaultPerformanceStorageUri(StorageType.JSON_FILE),
83 };
84 if (Configuration.objectHasOwnProperty(Configuration.getConfig(), 'performanceStorage')) {
85 storageConfiguration = {
86 ...storageConfiguration,
87 ...Configuration.getConfig().performanceStorage,
88 };
89 }
90 return storageConfiguration;
91 }
92
93 static getAutoReconnectMaxRetries(): number {
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 );
109 // Read conf
110 if (Configuration.objectHasOwnProperty(Configuration.getConfig(), 'autoReconnectMaxRetries')) {
111 return Configuration.getConfig().autoReconnectMaxRetries;
112 }
113 }
114
115 static getStationTemplateUrls(): StationTemplateUrl[] {
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[]);
125 Configuration.getConfig().stationTemplateUrls.forEach((stationUrl: StationTemplateUrl) => {
126 if (!Configuration.isUndefined(stationUrl['numberOfStation'])) {
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 );
132 }
133 });
134 // Read conf
135 return Configuration.getConfig().stationTemplateUrls;
136 }
137
138 static getWorker(): WorkerConfiguration {
139 Configuration.warnDeprecatedConfigurationKey(
140 'useWorkerPool',
141 null,
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"
168 );
169 Configuration.warnDeprecatedConfigurationKey(
170 'workerPoolSize;',
171 null,
172 "Use 'worker' section to define the worker pool maximum size instead"
173 );
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 );
184 let workerConfiguration: WorkerConfiguration = {
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')) {
218 workerConfiguration = { ...workerConfiguration, ...Configuration.getConfig().worker };
219 }
220 return workerConfiguration;
221 }
222
223 static getLogConsole(): boolean {
224 Configuration.warnDeprecatedConfigurationKey('consoleLog', null, "Use 'logConsole' instead");
225 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logConsole')
226 ? Configuration.getConfig().logConsole
227 : false;
228 }
229
230 static getLogFormat(): string {
231 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logFormat')
232 ? Configuration.getConfig().logFormat
233 : 'simple';
234 }
235
236 static getLogRotate(): boolean {
237 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logRotate')
238 ? Configuration.getConfig().logRotate
239 : true;
240 }
241
242 static getLogMaxFiles(): number {
243 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logMaxFiles')
244 ? Configuration.getConfig().logMaxFiles
245 : 7;
246 }
247
248 static getLogLevel(): string {
249 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logLevel')
250 ? Configuration.getConfig().logLevel.toLowerCase()
251 : 'info';
252 }
253
254 static getLogFile(): string {
255 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logFile')
256 ? Configuration.getConfig().logFile
257 : 'combined.log';
258 }
259
260 static getLogErrorFile(): string {
261 Configuration.warnDeprecatedConfigurationKey('errorFile', null, "Use 'logErrorFile' instead");
262 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logErrorFile')
263 ? Configuration.getConfig().logErrorFile
264 : 'error.log';
265 }
266
267 static getSupervisionUrls(): string | string[] {
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[]);
277 // Read conf
278 return Configuration.getConfig().supervisionUrls;
279 }
280
281 static getSupervisionUrlDistribution(): SupervisionUrlDistribution {
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;
298 }
299
300 private static logPrefix(): string {
301 return new Date().toLocaleString() + ' Simulator configuration |';
302 }
303
304 private static warnDeprecatedConfigurationKey(
305 key: string,
306 sectionName?: string,
307 logMsgToAppend = ''
308 ) {
309 if (
310 sectionName &&
311 !Configuration.isUndefined(Configuration.getConfig()[sectionName]) &&
312 !Configuration.isUndefined(
313 (Configuration.getConfig()[sectionName] as Record<string, unknown>)[key]
314 )
315 ) {
316 console.error(
317 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration key '${key}' usage in section '${sectionName}'${
318 logMsgToAppend && '. ' + logMsgToAppend
319 }}`
320 );
321 } else if (!Configuration.isUndefined(Configuration.getConfig()[key])) {
322 console.error(
323 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration key '${key}' usage${
324 logMsgToAppend && '. ' + logMsgToAppend
325 }}`
326 );
327 }
328 }
329
330 // Read the config file
331 private static getConfig(): ConfigurationData {
332 if (!Configuration.configuration) {
333 try {
334 Configuration.configuration = JSON.parse(
335 fs.readFileSync(Configuration.configurationFile, 'utf8')
336 ) as ConfigurationData;
337 } catch (error) {
338 Configuration.handleFileException(
339 Configuration.logPrefix(),
340 FileType.Configuration,
341 Configuration.configurationFile,
342 error as NodeJS.ErrnoException
343 );
344 }
345 if (!Configuration.configurationFileWatcher) {
346 Configuration.configurationFileWatcher = Configuration.getConfigurationFileWatcher();
347 }
348 }
349 return Configuration.configuration;
350 }
351
352 private static getConfigurationFileWatcher(): fs.FSWatcher {
353 try {
354 return fs.watch(Configuration.configurationFile, (event, filename): void => {
355 if (filename && event === 'change') {
356 // Nullify to force configuration file reading
357 Configuration.configuration = null;
358 if (!Configuration.isUndefined(Configuration.configurationChangeCallback)) {
359 Configuration.configurationChangeCallback().catch((error) => {
360 throw typeof error === 'string' ? new Error(error) : error;
361 });
362 }
363 }
364 });
365 } catch (error) {
366 Configuration.handleFileException(
367 Configuration.logPrefix(),
368 FileType.Configuration,
369 Configuration.configurationFile,
370 error as NodeJS.ErrnoException
371 );
372 }
373 }
374
375 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
376 const SQLiteFileName = `${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`;
377 switch (storageType) {
378 case StorageType.JSON_FILE:
379 return `file://${path.join(
380 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../'),
381 Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME
382 )}`;
383 case StorageType.SQLITE:
384 return `file://${path.join(
385 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../'),
386 SQLiteFileName
387 )}`;
388 default:
389 throw new Error(`Performance storage URI is mandatory with storage type '${storageType}'`);
390 }
391 }
392
393 private static isObject(item): boolean {
394 return item && typeof item === 'object' && Array.isArray(item) === false;
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
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 {
425 return typeof obj === 'undefined';
426 }
427
428 private static handleFileException(
429 logPrefix: string,
430 fileType: FileType,
431 filePath: string,
432 error: NodeJS.ErrnoException,
433 params: HandleErrorParams<EmptyObject> = { throwError: true }
434 ): void {
435 const prefix = logPrefix.length !== 0 ? logPrefix + ' ' : '';
436 if (error.code === 'ENOENT') {
437 console.error(
438 chalk.green(prefix) + chalk.red(fileType + ' file ' + filePath + ' not found: '),
439 error
440 );
441 } else if (error.code === 'EEXIST') {
442 console.error(
443 chalk.green(prefix) + chalk.red(fileType + ' file ' + filePath + ' already exists: '),
444 error
445 );
446 } else if (error.code === 'EACCES') {
447 console.error(
448 chalk.green(prefix) + chalk.red(fileType + ' file ' + filePath + ' access denied: '),
449 error
450 );
451 } else {
452 console.error(
453 chalk.green(prefix) + chalk.red(fileType + ' file ' + filePath + ' error: '),
454 error
455 );
456 }
457 if (params?.throwError) {
458 throw error;
459 }
460 }
461 }