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