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