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