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