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