refactor: cleanup imports
[e-mobility-charging-stations-simulator.git] / src / utils / Configuration.ts
CommitLineData
d972af76
JB
1import { type FSWatcher, readFileSync, watch } from 'node:fs';
2import { dirname, join, resolve } from 'node:path';
130783a7 3import { fileURLToPath } from 'node:url';
8114d10e
JB
4
5import chalk from 'chalk';
088ee3c1 6import merge from 'just-merge';
156cab2c 7import { WorkerChoiceStrategies } from 'poolifier';
8114d10e 8
878e026c 9import { Constants } from './Constants';
878e026c 10import { Utils } from './Utils';
83e00df1 11import {
268a74bb 12 ApplicationProtocol,
83e00df1 13 type ConfigurationData,
268a74bb 14 FileType,
3d48c1c1 15 type LogConfiguration,
83e00df1
JB
16 type StationTemplateUrl,
17 type StorageConfiguration,
268a74bb 18 StorageType,
e7aeea18 19 SupervisionUrlDistribution,
83e00df1
JB
20 type UIServerConfiguration,
21 type WorkerConfiguration,
268a74bb
JB
22} from '../types';
23import { WorkerConstants, WorkerProcessType } from '../worker';
7dde0b73 24
268a74bb 25export class Configuration {
d972af76
JB
26 private static configurationFile = join(
27 dirname(fileURLToPath(import.meta.url)),
e7aeea18
JB
28 'assets',
29 'config.json'
30 );
10068088 31
d972af76 32 private static configurationFileWatcher: FSWatcher | undefined;
6e0964c8 33 private static configuration: ConfigurationData | null = null;
e57acf6a
JB
34 private static configurationChangeCallback: () => Promise<void>;
35
d5bd1c00
JB
36 private constructor() {
37 // This is intentional
38 }
39
aa7d6d95 40 public static setConfigurationChangeCallback(cb: () => Promise<void>): void {
e57acf6a
JB
41 Configuration.configurationChangeCallback = cb;
42 }
7dde0b73 43
aa7d6d95 44 public static getUIServer(): UIServerConfiguration {
ef4932d8 45 if (Utils.hasOwnProp(Configuration.getConfig(), 'uiWebSocketServer')) {
66271092 46 console.error(
c5e52a07
JB
47 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
48 "Deprecated configuration section 'uiWebSocketServer' usage. Use 'uiServer' instead"
49 )}`
66271092
JB
50 );
51 }
675fa8e3 52 let uiServerConfiguration: UIServerConfiguration = {
b803eefa 53 enabled: false,
1f7fa4de 54 type: ApplicationProtocol.WS,
c127bd64 55 options: {
adbddcb4
JB
56 host: Constants.DEFAULT_UI_SERVER_HOST,
57 port: Constants.DEFAULT_UI_SERVER_PORT,
c127bd64 58 },
6a49ad23 59 };
ef4932d8 60 if (Utils.hasOwnProp(Configuration.getConfig(), 'uiServer')) {
598c886d
JB
61 uiServerConfiguration = merge<UIServerConfiguration>(
62 uiServerConfiguration,
63 Configuration.getConfig()?.uiServer
64 );
6a49ad23 65 }
dada83ec 66 if (Utils.isCFEnvironment() === true) {
72092cfc 67 delete uiServerConfiguration.options?.host;
b803eefa
JB
68 uiServerConfiguration.options.port = parseInt(process.env.PORT);
69 }
675fa8e3 70 return uiServerConfiguration;
6a49ad23
JB
71 }
72
aa7d6d95 73 public static getPerformanceStorage(): StorageConfiguration {
e7aeea18 74 Configuration.warnDeprecatedConfigurationKey('URI', 'performanceStorage', "Use 'uri' instead");
6a49ad23
JB
75 let storageConfiguration: StorageConfiguration = {
76 enabled: false,
77 type: StorageType.JSON_FILE,
e7aeea18 78 uri: this.getDefaultPerformanceStorageUri(StorageType.JSON_FILE),
6a49ad23 79 };
ef4932d8 80 if (Utils.hasOwnProp(Configuration.getConfig(), 'performanceStorage')) {
e7aeea18 81 storageConfiguration = {
1ba1e8fb 82 ...storageConfiguration,
1895299d 83 ...Configuration.getConfig()?.performanceStorage,
f682b2dc
JB
84 ...(Configuration.getConfig()?.performanceStorage?.type === StorageType.JSON_FILE &&
85 Configuration.getConfig()?.performanceStorage?.uri && {
e8044a69 86 uri: Configuration.buildPerformanceUriFilePath(
f682b2dc 87 new URL(Configuration.getConfig()?.performanceStorage?.uri).pathname
e8044a69 88 ),
f682b2dc 89 }),
72f041bd 90 };
72f041bd
JB
91 }
92 return storageConfiguration;
7dde0b73
JB
93 }
94
aa7d6d95 95 public static getAutoReconnectMaxRetries(): number | undefined {
e7aeea18
JB
96 Configuration.warnDeprecatedConfigurationKey(
97 'autoReconnectTimeout',
1895299d 98 undefined,
e7aeea18
JB
99 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
100 );
101 Configuration.warnDeprecatedConfigurationKey(
102 'connectionTimeout',
1895299d 103 undefined,
e7aeea18
JB
104 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
105 );
106 Configuration.warnDeprecatedConfigurationKey(
107 'autoReconnectMaxRetries',
1895299d 108 undefined,
e7aeea18
JB
109 'Use it in charging station template instead'
110 );
7dde0b73 111 // Read conf
ef4932d8 112 if (Utils.hasOwnProp(Configuration.getConfig(), 'autoReconnectMaxRetries')) {
1895299d 113 return Configuration.getConfig()?.autoReconnectMaxRetries;
3574dfd3 114 }
7dde0b73
JB
115 }
116
aa7d6d95 117 public static getStationTemplateUrls(): StationTemplateUrl[] | undefined {
e7aeea18
JB
118 Configuration.warnDeprecatedConfigurationKey(
119 'stationTemplateURLs',
1895299d 120 undefined,
e7aeea18
JB
121 "Use 'stationTemplateUrls' instead"
122 );
dada83ec 123 !Utils.isUndefined(Configuration.getConfig()['stationTemplateURLs']) &&
e7aeea18
JB
124 (Configuration.getConfig().stationTemplateUrls = Configuration.getConfig()[
125 'stationTemplateURLs'
617cad0c 126 ] as StationTemplateUrl[]);
7436ee0d
JB
127 Configuration.getConfig().stationTemplateUrls.forEach(
128 (stationTemplateUrl: StationTemplateUrl) => {
129 if (!Utils.isUndefined(stationTemplateUrl['numberOfStation'])) {
130 console.error(
c5e52a07
JB
131 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
132 `Deprecated configuration key 'numberOfStation' usage for template file '${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use 'numberOfStations' instead`
133 )}`
7436ee0d
JB
134 );
135 }
eb3937cb 136 }
7436ee0d 137 );
7dde0b73 138 // Read conf
1895299d 139 return Configuration.getConfig()?.stationTemplateUrls;
7dde0b73
JB
140 }
141
3d48c1c1
JB
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,
be623cdc 181 "Use 'log' section to define the log rotation enablement instead"
3d48c1c1
JB
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 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logEnabled') && {
204 enabled: Configuration.getConfig()?.logEnabled,
205 }),
206 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logFile') && {
207 file: Configuration.getConfig()?.logFile,
208 }),
209 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logErrorFile') && {
210 errorFile: Configuration.getConfig()?.logErrorFile,
211 }),
212 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logStatisticsInterval') && {
213 statisticsInterval: Configuration.getConfig()?.logStatisticsInterval,
214 }),
215 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logLevel') && {
216 level: Configuration.getConfig()?.logLevel,
217 }),
218 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logConsole') && {
219 console: Configuration.getConfig()?.logConsole,
220 }),
221 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logFormat') && {
222 format: Configuration.getConfig()?.logFormat,
223 }),
224 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logRotate') && {
225 rotate: Configuration.getConfig()?.logRotate,
226 }),
227 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logMaxFiles') && {
228 maxFiles: Configuration.getConfig()?.logMaxFiles,
229 }),
230 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logMaxSize') && {
231 maxSize: Configuration.getConfig()?.logMaxSize,
232 }),
233 };
234 const logConfiguration: LogConfiguration = {
235 ...defaultLogConfiguration,
236 ...deprecatedLogConfiguration,
25baaaf2 237 ...(Utils.hasOwnProp(Configuration.getConfig(), 'log') && Configuration.getConfig()?.log),
3d48c1c1
JB
238 };
239 return logConfiguration;
240 }
241
aa7d6d95 242 public static getWorker(): WorkerConfiguration {
e7aeea18 243 Configuration.warnDeprecatedConfigurationKey(
e80bc579 244 'useWorkerPool',
1895299d 245 undefined,
cf2a5d9b
JB
246 "Use 'worker' section to define the type of worker process model instead"
247 );
248 Configuration.warnDeprecatedConfigurationKey(
249 'workerProcess',
1895299d 250 undefined,
cf2a5d9b
JB
251 "Use 'worker' section to define the type of worker process model instead"
252 );
253 Configuration.warnDeprecatedConfigurationKey(
254 'workerStartDelay',
1895299d 255 undefined,
cf2a5d9b
JB
256 "Use 'worker' section to define the worker start delay instead"
257 );
258 Configuration.warnDeprecatedConfigurationKey(
259 'chargingStationsPerWorker',
1895299d 260 undefined,
cf2a5d9b
JB
261 "Use 'worker' section to define the number of element(s) per worker instead"
262 );
263 Configuration.warnDeprecatedConfigurationKey(
264 'elementStartDelay',
1895299d 265 undefined,
cf2a5d9b
JB
266 "Use 'worker' section to define the worker's element start delay instead"
267 );
268 Configuration.warnDeprecatedConfigurationKey(
269 'workerPoolMinSize',
1895299d 270 undefined,
cf2a5d9b 271 "Use 'worker' section to define the worker pool minimum size instead"
e7aeea18 272 );
e7aeea18
JB
273 Configuration.warnDeprecatedConfigurationKey(
274 'workerPoolSize;',
1895299d 275 undefined,
cf2a5d9b 276 "Use 'worker' section to define the worker pool maximum size instead"
e7aeea18 277 );
cf2a5d9b
JB
278 Configuration.warnDeprecatedConfigurationKey(
279 'workerPoolMaxSize;',
1895299d 280 undefined,
cf2a5d9b
JB
281 "Use 'worker' section to define the worker pool maximum size instead"
282 );
283 Configuration.warnDeprecatedConfigurationKey(
284 'workerPoolStrategy;',
1895299d 285 undefined,
cf2a5d9b
JB
286 "Use 'worker' section to define the worker pool strategy instead"
287 );
3d48c1c1
JB
288 const defaultWorkerConfiguration: WorkerConfiguration = {
289 processType: WorkerProcessType.workerSet,
290 startDelay: WorkerConstants.DEFAULT_WORKER_START_DELAY,
291 elementsPerWorker: WorkerConstants.DEFAULT_ELEMENTS_PER_WORKER,
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 ...(Utils.hasOwnProp(Configuration.getConfig(), 'workerProcess') && {
299 processType: Configuration.getConfig()?.workerProcess,
300 }),
301 ...(Utils.hasOwnProp(Configuration.getConfig(), 'workerStartDelay') && {
302 startDelay: Configuration.getConfig()?.workerStartDelay,
303 }),
304 ...(Utils.hasOwnProp(Configuration.getConfig(), 'chargingStationsPerWorker') && {
305 elementsPerWorker: Configuration.getConfig()?.chargingStationsPerWorker,
306 }),
307 ...(Utils.hasOwnProp(Configuration.getConfig(), 'elementStartDelay') && {
308 elementStartDelay: Configuration.getConfig()?.elementStartDelay,
309 }),
310 ...(Utils.hasOwnProp(Configuration.getConfig(), 'workerPoolMinSize') && {
311 poolMinSize: Configuration.getConfig()?.workerPoolMinSize,
312 }),
313 ...(Utils.hasOwnProp(Configuration.getConfig(), 'workerPoolMaxSize') && {
314 poolMaxSize: Configuration.getConfig()?.workerPoolMaxSize,
315 }),
316 ...(Utils.hasOwnProp(Configuration.getConfig(), 'workerPoolStrategy') && {
317 poolStrategy:
318 Configuration.getConfig()?.workerPoolStrategy ?? WorkerChoiceStrategies.ROUND_ROBIN,
319 }),
320 };
321 const workerConfiguration: WorkerConfiguration = {
322 ...defaultWorkerConfiguration,
323 ...deprecatedWorkerConfiguration,
25baaaf2
JB
324 ...(Utils.hasOwnProp(Configuration.getConfig(), 'worker') &&
325 Configuration.getConfig()?.worker),
cf2a5d9b 326 };
cf2a5d9b 327 return workerConfiguration;
3d2ff9e4
J
328 }
329
aa7d6d95
JB
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
aa7d6d95 340 public static getSupervisionUrls(): string | string[] | undefined {
e7aeea18
JB
341 Configuration.warnDeprecatedConfigurationKey(
342 'supervisionURLs',
1895299d 343 undefined,
e7aeea18
JB
344 "Use 'supervisionUrls' instead"
345 );
dada83ec 346 !Utils.isUndefined(Configuration.getConfig()['supervisionURLs']) &&
1895299d
JB
347 (Configuration.getConfig().supervisionUrls = Configuration.getConfig()['supervisionURLs'] as
348 | string
349 | string[]);
7dde0b73 350 // Read conf
1895299d 351 return Configuration.getConfig()?.supervisionUrls;
7dde0b73
JB
352 }
353
aa7d6d95 354 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution | undefined {
e7aeea18
JB
355 Configuration.warnDeprecatedConfigurationKey(
356 'distributeStationToTenantEqually',
1895299d 357 undefined,
e7aeea18
JB
358 "Use 'supervisionUrlDistribution' instead"
359 );
360 Configuration.warnDeprecatedConfigurationKey(
361 'distributeStationsToTenantsEqually',
1895299d 362 undefined,
e7aeea18
JB
363 "Use 'supervisionUrlDistribution' instead"
364 );
ef4932d8 365 return Utils.hasOwnProp(Configuration.getConfig(), 'supervisionUrlDistribution')
1895299d 366 ? Configuration.getConfig()?.supervisionUrlDistribution
e7aeea18 367 : SupervisionUrlDistribution.ROUND_ROBIN;
7dde0b73 368 }
eb3937cb 369
8b7072dc 370 private static logPrefix = (): string => {
14ecae6a 371 return `${new Date().toLocaleString()} Simulator configuration |`;
8b7072dc 372 };
23132a44 373
e7aeea18
JB
374 private static warnDeprecatedConfigurationKey(
375 key: string,
376 sectionName?: string,
377 logMsgToAppend = ''
378 ) {
e7aeea18
JB
379 if (
380 sectionName &&
dada83ec
JB
381 !Utils.isUndefined(Configuration.getConfig()[sectionName]) &&
382 !Utils.isUndefined((Configuration.getConfig()[sectionName] as Record<string, unknown>)[key])
e7aeea18
JB
383 ) {
384 console.error(
c5e52a07
JB
385 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
386 `Deprecated configuration key '${key}' usage in section '${sectionName}'${
387 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
388 }`
389 )}`
e7aeea18 390 );
dada83ec 391 } else if (!Utils.isUndefined(Configuration.getConfig()[key])) {
e7aeea18 392 console.error(
c5e52a07
JB
393 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
394 `Deprecated configuration key '${key}' usage${
395 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
396 }`
397 )}`
e7aeea18 398 );
eb3937cb
JB
399 }
400 }
401
402 // Read the config file
1895299d 403 private static getConfig(): ConfigurationData | null {
eb3937cb 404 if (!Configuration.configuration) {
23132a44 405 try {
e7aeea18 406 Configuration.configuration = JSON.parse(
d972af76 407 readFileSync(Configuration.configurationFile, 'utf8')
e7aeea18 408 ) as ConfigurationData;
23132a44 409 } catch (error) {
69074173 410 Configuration.handleFileException(
a95873d8 411 Configuration.configurationFile,
7164966d
JB
412 FileType.Configuration,
413 error as NodeJS.ErrnoException,
69074173 414 Configuration.logPrefix()
e7aeea18 415 );
23132a44 416 }
ded13d97
JB
417 if (!Configuration.configurationFileWatcher) {
418 Configuration.configurationFileWatcher = Configuration.getConfigurationFileWatcher();
419 }
eb3937cb
JB
420 }
421 return Configuration.configuration;
422 }
963ee397 423
d972af76 424 private static getConfigurationFileWatcher(): FSWatcher | undefined {
23132a44 425 try {
d972af76 426 return watch(Configuration.configurationFile, (event, filename): void => {
8d54dcc0 427 if (filename?.trim().length > 0 && event === 'change') {
3ec10737
JB
428 // Nullify to force configuration file reading
429 Configuration.configuration = null;
dada83ec 430 if (!Utils.isUndefined(Configuration.configurationChangeCallback)) {
72092cfc 431 Configuration.configurationChangeCallback().catch((error) => {
dcaf96dc
JB
432 throw typeof error === 'string' ? new Error(error) : error;
433 });
3ec10737 434 }
23132a44
JB
435 }
436 });
437 } catch (error) {
69074173 438 Configuration.handleFileException(
a95873d8 439 Configuration.configurationFile,
7164966d
JB
440 FileType.Configuration,
441 error as NodeJS.ErrnoException,
69074173 442 Configuration.logPrefix()
e7aeea18 443 );
23132a44 444 }
ded13d97
JB
445 }
446
69074173
JB
447 private static handleFileException(
448 file: string,
449 fileType: FileType,
450 error: NodeJS.ErrnoException,
451 logPrefix: string
452 ): void {
453 const prefix = Utils.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;
7b5dbe91
JB
465 case 'EPERM':
466 logMsg = `${fileType} file ${file} permission denied:`;
467 break;
69074173
JB
468 default:
469 logMsg = `${fileType} file ${file} error:`;
470 }
7b5dbe91
JB
471 console.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
472 throw error;
69074173
JB
473 }
474
1f5df42a 475 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
d5603918
JB
476 switch (storageType) {
477 case StorageType.JSON_FILE:
e8044a69 478 return Configuration.buildPerformanceUriFilePath(
53b8a4fd 479 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}`
e8044a69 480 );
d5603918 481 case StorageType.SQLITE:
e8044a69 482 return Configuration.buildPerformanceUriFilePath(
53b8a4fd 483 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`
e8044a69 484 );
d5603918
JB
485 default:
486 throw new Error(`Performance storage URI is mandatory with storage type '${storageType}'`);
487 }
488 }
e8044a69
JB
489
490 private static buildPerformanceUriFilePath(file: string) {
d972af76 491 return `file://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;
e8044a69 492 }
7dde0b73 493}