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