6b8023ab0b2911cb7bf54ef38d21aaaddcfb0c67
[e-mobility-charging-stations-simulator.git] / src / utils / Configuration.ts
1 import { type FSWatcher, readFileSync, watch } from 'node:fs';
2 import { dirname, join, resolve } from 'node:path';
3 import { fileURLToPath } from 'node:url';
4
5 import chalk from 'chalk';
6 import merge from 'just-merge';
7
8 import { Constants } from './Constants';
9 import {
10 hasOwnProp,
11 isCFEnvironment,
12 isNotEmptyString,
13 isUndefined,
14 logPrefix,
15 once,
16 } from './Utils';
17 import {
18 ApplicationProtocol,
19 type ConfigurationData,
20 ConfigurationSection,
21 FileType,
22 type LogConfiguration,
23 type StationTemplateUrl,
24 type StorageConfiguration,
25 StorageType,
26 SupervisionUrlDistribution,
27 type UIServerConfiguration,
28 type WorkerConfiguration,
29 } from '../types';
30 import {
31 DEFAULT_ELEMENT_START_DELAY,
32 DEFAULT_POOL_MAX_SIZE,
33 DEFAULT_POOL_MIN_SIZE,
34 DEFAULT_WORKER_START_DELAY,
35 WorkerProcessType,
36 } from '../worker';
37
38 type ConfigurationSectionType =
39 | LogConfiguration
40 | StorageConfiguration
41 | WorkerConfiguration
42 | UIServerConfiguration;
43
44 export class Configuration {
45 public static configurationChangeCallback: () => Promise<void>;
46
47 private static configurationFile = join(
48 dirname(fileURLToPath(import.meta.url)),
49 'assets',
50 'config.json',
51 );
52
53 private static configurationData?: ConfigurationData;
54 private static configurationFileWatcher?: FSWatcher;
55 private static configurationSectionCache = new Map<
56 ConfigurationSection,
57 ConfigurationSectionType
58 >([
59 [ConfigurationSection.log, Configuration.buildLogSection()],
60 [ConfigurationSection.performanceStorage, Configuration.buildPerformanceStorageSection()],
61 [ConfigurationSection.worker, Configuration.buildWorkerSection()],
62 [ConfigurationSection.uiServer, Configuration.buildUIServerSection()],
63 ]);
64
65 private constructor() {
66 // This is intentional
67 }
68
69 public static getConfigurationSection<T extends ConfigurationSectionType>(
70 sectionName: ConfigurationSection,
71 ): T {
72 if (!Configuration.isConfigurationSectionCached(sectionName)) {
73 Configuration.cacheConfigurationSection(sectionName);
74 }
75 return Configuration.configurationSectionCache.get(sectionName) as T;
76 }
77
78 public static getStationTemplateUrls(): StationTemplateUrl[] | undefined {
79 const checkDeprecatedConfigurationKeysOnce = once(
80 Configuration.checkDeprecatedConfigurationKeys.bind(Configuration),
81 Configuration,
82 );
83 checkDeprecatedConfigurationKeysOnce();
84 return Configuration.getConfigurationData()?.stationTemplateUrls;
85 }
86
87 public static getSupervisionUrls(): string | string[] | undefined {
88 if (
89 !isUndefined(
90 Configuration.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData],
91 )
92 ) {
93 Configuration.getConfigurationData()!.supervisionUrls = Configuration.getConfigurationData()![
94 'supervisionURLs' as keyof ConfigurationData
95 ] as string | string[];
96 }
97 return Configuration.getConfigurationData()?.supervisionUrls;
98 }
99
100 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution | undefined {
101 return hasOwnProp(Configuration.getConfigurationData(), 'supervisionUrlDistribution')
102 ? Configuration.getConfigurationData()?.supervisionUrlDistribution
103 : SupervisionUrlDistribution.ROUND_ROBIN;
104 }
105
106 public static workerPoolInUse(): boolean {
107 return [WorkerProcessType.dynamicPool, WorkerProcessType.staticPool].includes(
108 Configuration.getConfigurationSection<WorkerConfiguration>(ConfigurationSection.worker)
109 .processType!,
110 );
111 }
112
113 public static workerDynamicPoolInUse(): boolean {
114 return (
115 Configuration.getConfigurationSection<WorkerConfiguration>(ConfigurationSection.worker)
116 .processType === WorkerProcessType.dynamicPool
117 );
118 }
119
120 private static logPrefix = (): string => {
121 return logPrefix(' Simulator configuration |');
122 };
123
124 private static isConfigurationSectionCached(sectionName: ConfigurationSection): boolean {
125 return Configuration.configurationSectionCache.has(sectionName);
126 }
127
128 private static cacheConfigurationSection(sectionName: ConfigurationSection): void {
129 switch (sectionName) {
130 case ConfigurationSection.log:
131 Configuration.configurationSectionCache.set(sectionName, Configuration.buildLogSection());
132 break;
133 case ConfigurationSection.performanceStorage:
134 Configuration.configurationSectionCache.set(
135 sectionName,
136 Configuration.buildPerformanceStorageSection(),
137 );
138 break;
139 case ConfigurationSection.worker:
140 Configuration.configurationSectionCache.set(
141 sectionName,
142 Configuration.buildWorkerSection(),
143 );
144 break;
145 case ConfigurationSection.uiServer:
146 Configuration.configurationSectionCache.set(
147 sectionName,
148 Configuration.buildUIServerSection(),
149 );
150 break;
151 default:
152 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
153 throw new Error(`Unknown configuration section '${sectionName}'`);
154 }
155 }
156
157 private static buildUIServerSection(): UIServerConfiguration {
158 let uiServerConfiguration: UIServerConfiguration = {
159 enabled: false,
160 type: ApplicationProtocol.WS,
161 options: {
162 host: Constants.DEFAULT_UI_SERVER_HOST,
163 port: Constants.DEFAULT_UI_SERVER_PORT,
164 },
165 };
166 if (hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.uiServer)) {
167 uiServerConfiguration = merge<UIServerConfiguration>(
168 uiServerConfiguration,
169 Configuration.getConfigurationData()!.uiServer!,
170 );
171 }
172 if (isCFEnvironment() === true) {
173 delete uiServerConfiguration.options?.host;
174 uiServerConfiguration.options!.port = parseInt(process.env.PORT!);
175 }
176 return uiServerConfiguration;
177 }
178
179 private static buildPerformanceStorageSection(): StorageConfiguration {
180 let storageConfiguration: StorageConfiguration = {
181 enabled: false,
182 type: StorageType.JSON_FILE,
183 uri: Configuration.getDefaultPerformanceStorageUri(StorageType.JSON_FILE),
184 };
185 if (hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.performanceStorage)) {
186 storageConfiguration = {
187 ...storageConfiguration,
188 ...Configuration.getConfigurationData()?.performanceStorage,
189 ...(Configuration.getConfigurationData()?.performanceStorage?.type ===
190 StorageType.JSON_FILE &&
191 Configuration.getConfigurationData()?.performanceStorage?.uri && {
192 uri: Configuration.buildPerformanceUriFilePath(
193 new URL(Configuration.getConfigurationData()!.performanceStorage!.uri!).pathname,
194 ),
195 }),
196 };
197 }
198 return storageConfiguration;
199 }
200
201 private static buildLogSection(): LogConfiguration {
202 const defaultLogConfiguration: LogConfiguration = {
203 enabled: true,
204 file: 'logs/combined.log',
205 errorFile: 'logs/error.log',
206 statisticsInterval: Constants.DEFAULT_LOG_STATISTICS_INTERVAL,
207 level: 'info',
208 format: 'simple',
209 rotate: true,
210 };
211 const deprecatedLogConfiguration: LogConfiguration = {
212 ...(hasOwnProp(Configuration.getConfigurationData(), 'logEnabled') && {
213 enabled: Configuration.getConfigurationData()?.logEnabled,
214 }),
215 ...(hasOwnProp(Configuration.getConfigurationData(), 'logFile') && {
216 file: Configuration.getConfigurationData()?.logFile,
217 }),
218 ...(hasOwnProp(Configuration.getConfigurationData(), 'logErrorFile') && {
219 errorFile: Configuration.getConfigurationData()?.logErrorFile,
220 }),
221 ...(hasOwnProp(Configuration.getConfigurationData(), 'logStatisticsInterval') && {
222 statisticsInterval: Configuration.getConfigurationData()?.logStatisticsInterval,
223 }),
224 ...(hasOwnProp(Configuration.getConfigurationData(), 'logLevel') && {
225 level: Configuration.getConfigurationData()?.logLevel,
226 }),
227 ...(hasOwnProp(Configuration.getConfigurationData(), 'logConsole') && {
228 console: Configuration.getConfigurationData()?.logConsole,
229 }),
230 ...(hasOwnProp(Configuration.getConfigurationData(), 'logFormat') && {
231 format: Configuration.getConfigurationData()?.logFormat,
232 }),
233 ...(hasOwnProp(Configuration.getConfigurationData(), 'logRotate') && {
234 rotate: Configuration.getConfigurationData()?.logRotate,
235 }),
236 ...(hasOwnProp(Configuration.getConfigurationData(), 'logMaxFiles') && {
237 maxFiles: Configuration.getConfigurationData()?.logMaxFiles,
238 }),
239 ...(hasOwnProp(Configuration.getConfigurationData(), 'logMaxSize') && {
240 maxSize: Configuration.getConfigurationData()?.logMaxSize,
241 }),
242 };
243 const logConfiguration: LogConfiguration = {
244 ...defaultLogConfiguration,
245 ...deprecatedLogConfiguration,
246 ...(hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.log) &&
247 Configuration.getConfigurationData()?.log),
248 };
249 return logConfiguration;
250 }
251
252 private static buildWorkerSection(): WorkerConfiguration {
253 const defaultWorkerConfiguration: WorkerConfiguration = {
254 processType: WorkerProcessType.workerSet,
255 startDelay: DEFAULT_WORKER_START_DELAY,
256 elementsPerWorker: 'auto',
257 elementStartDelay: DEFAULT_ELEMENT_START_DELAY,
258 poolMinSize: DEFAULT_POOL_MIN_SIZE,
259 poolMaxSize: DEFAULT_POOL_MAX_SIZE,
260 };
261 const deprecatedWorkerConfiguration: WorkerConfiguration = {
262 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerProcess') && {
263 processType: Configuration.getConfigurationData()?.workerProcess,
264 }),
265 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerStartDelay') && {
266 startDelay: Configuration.getConfigurationData()?.workerStartDelay,
267 }),
268 ...(hasOwnProp(Configuration.getConfigurationData(), 'chargingStationsPerWorker') && {
269 elementsPerWorker: Configuration.getConfigurationData()?.chargingStationsPerWorker,
270 }),
271 ...(hasOwnProp(Configuration.getConfigurationData(), 'elementStartDelay') && {
272 elementStartDelay: Configuration.getConfigurationData()?.elementStartDelay,
273 }),
274 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerPoolMinSize') && {
275 poolMinSize: Configuration.getConfigurationData()?.workerPoolMinSize,
276 }),
277 ...(hasOwnProp(Configuration.getConfigurationData(), 'workerPoolMaxSize') && {
278 poolMaxSize: Configuration.getConfigurationData()?.workerPoolMaxSize,
279 }),
280 };
281 hasOwnProp(Configuration.getConfigurationData(), 'workerPoolStrategy') &&
282 delete Configuration.getConfigurationData()?.workerPoolStrategy;
283 const workerConfiguration: WorkerConfiguration = {
284 ...defaultWorkerConfiguration,
285 ...deprecatedWorkerConfiguration,
286 ...(hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.worker) &&
287 Configuration.getConfigurationData()?.worker),
288 };
289 if (!Object.values(WorkerProcessType).includes(workerConfiguration.processType!)) {
290 throw new SyntaxError(
291 `Invalid worker process type '${workerConfiguration.processType}' defined in configuration`,
292 );
293 }
294 return workerConfiguration;
295 }
296
297 private static checkDeprecatedConfigurationKeys() {
298 // connection timeout
299 Configuration.warnDeprecatedConfigurationKey(
300 'autoReconnectTimeout',
301 undefined,
302 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
303 );
304 Configuration.warnDeprecatedConfigurationKey(
305 'connectionTimeout',
306 undefined,
307 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
308 );
309 // connection retries
310 Configuration.warnDeprecatedConfigurationKey(
311 'autoReconnectMaxRetries',
312 undefined,
313 'Use it in charging station template instead',
314 );
315 // station template url(s)
316 Configuration.warnDeprecatedConfigurationKey(
317 'stationTemplateURLs',
318 undefined,
319 "Use 'stationTemplateUrls' instead",
320 );
321 !isUndefined(
322 Configuration.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData],
323 ) &&
324 (Configuration.getConfigurationData()!.stationTemplateUrls =
325 Configuration.getConfigurationData()![
326 'stationTemplateURLs' as keyof ConfigurationData
327 ] as StationTemplateUrl[]);
328 Configuration.getConfigurationData()?.stationTemplateUrls.forEach(
329 (stationTemplateUrl: StationTemplateUrl) => {
330 if (!isUndefined(stationTemplateUrl?.['numberOfStation' as keyof StationTemplateUrl])) {
331 console.error(
332 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
333 `Deprecated configuration key 'numberOfStation' usage for template file '${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use 'numberOfStations' instead`,
334 )}`,
335 );
336 }
337 },
338 );
339 // supervision url(s)
340 Configuration.warnDeprecatedConfigurationKey(
341 'supervisionURLs',
342 undefined,
343 "Use 'supervisionUrls' instead",
344 );
345 // supervision urls distribution
346 Configuration.warnDeprecatedConfigurationKey(
347 'distributeStationToTenantEqually',
348 undefined,
349 "Use 'supervisionUrlDistribution' instead",
350 );
351 Configuration.warnDeprecatedConfigurationKey(
352 'distributeStationsToTenantsEqually',
353 undefined,
354 "Use 'supervisionUrlDistribution' instead",
355 );
356 // worker section
357 Configuration.warnDeprecatedConfigurationKey(
358 'useWorkerPool',
359 undefined,
360 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
361 );
362 Configuration.warnDeprecatedConfigurationKey(
363 'workerProcess',
364 undefined,
365 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
366 );
367 Configuration.warnDeprecatedConfigurationKey(
368 'workerStartDelay',
369 undefined,
370 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`,
371 );
372 Configuration.warnDeprecatedConfigurationKey(
373 'chargingStationsPerWorker',
374 undefined,
375 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`,
376 );
377 Configuration.warnDeprecatedConfigurationKey(
378 'elementStartDelay',
379 undefined,
380 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`,
381 );
382 Configuration.warnDeprecatedConfigurationKey(
383 'workerPoolMinSize',
384 undefined,
385 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`,
386 );
387 Configuration.warnDeprecatedConfigurationKey(
388 'workerPoolSize;',
389 undefined,
390 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
391 );
392 Configuration.warnDeprecatedConfigurationKey(
393 'workerPoolMaxSize;',
394 undefined,
395 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
396 );
397 Configuration.warnDeprecatedConfigurationKey(
398 'workerPoolStrategy;',
399 undefined,
400 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`,
401 );
402 Configuration.warnDeprecatedConfigurationKey(
403 'poolStrategy',
404 ConfigurationSection.worker,
405 'Not publicly exposed to end users',
406 );
407 // log section
408 Configuration.warnDeprecatedConfigurationKey(
409 'logEnabled',
410 undefined,
411 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
412 );
413 Configuration.warnDeprecatedConfigurationKey(
414 'logFile',
415 undefined,
416 `Use '${ConfigurationSection.log}' section to define the log file instead`,
417 );
418 Configuration.warnDeprecatedConfigurationKey(
419 'logErrorFile',
420 undefined,
421 `Use '${ConfigurationSection.log}' section to define the log error file instead`,
422 );
423 Configuration.warnDeprecatedConfigurationKey(
424 'logConsole',
425 undefined,
426 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
427 );
428 Configuration.warnDeprecatedConfigurationKey(
429 'logStatisticsInterval',
430 undefined,
431 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
432 );
433 Configuration.warnDeprecatedConfigurationKey(
434 'logLevel',
435 undefined,
436 `Use '${ConfigurationSection.log}' section to define the log level instead`,
437 );
438 Configuration.warnDeprecatedConfigurationKey(
439 'logFormat',
440 undefined,
441 `Use '${ConfigurationSection.log}' section to define the log format instead`,
442 );
443 Configuration.warnDeprecatedConfigurationKey(
444 'logRotate',
445 undefined,
446 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
447 );
448 Configuration.warnDeprecatedConfigurationKey(
449 'logMaxFiles',
450 undefined,
451 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
452 );
453 Configuration.warnDeprecatedConfigurationKey(
454 'logMaxSize',
455 undefined,
456 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
457 );
458 // performanceStorage section
459 Configuration.warnDeprecatedConfigurationKey(
460 'URI',
461 ConfigurationSection.performanceStorage,
462 "Use 'uri' instead",
463 );
464 // uiServer section
465 if (hasOwnProp(Configuration.getConfigurationData(), 'uiWebSocketServer')) {
466 console.error(
467 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
468 `Deprecated configuration section 'uiWebSocketServer' usage. Use '${ConfigurationSection.uiServer}' instead`,
469 )}`,
470 );
471 }
472 }
473
474 private static warnDeprecatedConfigurationKey(
475 key: string,
476 sectionName?: string,
477 logMsgToAppend = '',
478 ) {
479 if (
480 sectionName &&
481 !isUndefined(
482 Configuration.getConfigurationData()?.[sectionName as keyof ConfigurationData],
483 ) &&
484 !isUndefined(
485 (
486 Configuration.getConfigurationData()?.[sectionName as keyof ConfigurationData] as Record<
487 string,
488 unknown
489 >
490 )?.[key],
491 )
492 ) {
493 console.error(
494 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
495 `Deprecated configuration key '${key}' usage in section '${sectionName}'${
496 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
497 }`,
498 )}`,
499 );
500 } else if (
501 !isUndefined(Configuration.getConfigurationData()?.[key as keyof ConfigurationData])
502 ) {
503 console.error(
504 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
505 `Deprecated configuration key '${key}' usage${
506 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
507 }`,
508 )}`,
509 );
510 }
511 }
512
513 private static getConfigurationData(): ConfigurationData | undefined {
514 if (!Configuration.configurationData) {
515 try {
516 Configuration.configurationData = JSON.parse(
517 readFileSync(Configuration.configurationFile, 'utf8'),
518 ) as ConfigurationData;
519 if (!Configuration.configurationFileWatcher) {
520 Configuration.configurationFileWatcher = Configuration.getConfigurationFileWatcher();
521 }
522 } catch (error) {
523 Configuration.handleFileException(
524 Configuration.configurationFile,
525 FileType.Configuration,
526 error as NodeJS.ErrnoException,
527 Configuration.logPrefix(),
528 );
529 }
530 }
531 return Configuration.configurationData;
532 }
533
534 private static getConfigurationFileWatcher(): FSWatcher | undefined {
535 try {
536 return watch(Configuration.configurationFile, (event, filename): void => {
537 if (filename!.trim()!.length > 0 && event === 'change') {
538 delete Configuration.configurationData;
539 Configuration.configurationSectionCache.clear();
540 if (!isUndefined(Configuration.configurationChangeCallback)) {
541 Configuration.configurationChangeCallback().catch((error) => {
542 throw typeof error === 'string' ? new Error(error) : error;
543 });
544 }
545 }
546 });
547 } catch (error) {
548 Configuration.handleFileException(
549 Configuration.configurationFile,
550 FileType.Configuration,
551 error as NodeJS.ErrnoException,
552 Configuration.logPrefix(),
553 );
554 }
555 }
556
557 private static handleFileException(
558 file: string,
559 fileType: FileType,
560 error: NodeJS.ErrnoException,
561 logPfx: string,
562 ): void {
563 const prefix = isNotEmptyString(logPfx) ? `${logPfx} ` : '';
564 let logMsg: string;
565 switch (error.code) {
566 case 'ENOENT':
567 logMsg = `${fileType} file ${file} not found: `;
568 break;
569 case 'EEXIST':
570 logMsg = `${fileType} file ${file} already exists: `;
571 break;
572 case 'EACCES':
573 logMsg = `${fileType} file ${file} access denied: `;
574 break;
575 case 'EPERM':
576 logMsg = `${fileType} file ${file} permission denied: `;
577 break;
578 default:
579 logMsg = `${fileType} file ${file} error: `;
580 }
581 console.error(`${chalk.green(prefix)}${chalk.red(logMsg)}`, error);
582 throw error;
583 }
584
585 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
586 switch (storageType) {
587 case StorageType.JSON_FILE:
588 return Configuration.buildPerformanceUriFilePath(
589 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}`,
590 );
591 case StorageType.SQLITE:
592 return Configuration.buildPerformanceUriFilePath(
593 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`,
594 );
595 default:
596 throw new Error(`Unsupported storage type '${storageType}'`);
597 }
598 }
599
600 private static buildPerformanceUriFilePath(file: string) {
601 return `file://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;
602 }
603 }