fix: does not override ATG configuration with default one
[e-mobility-charging-stations-simulator.git] / src / utils / Configuration.ts
CommitLineData
130783a7
JB
1import fs from 'node:fs';
2import path from 'node:path';
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,
83e00df1
JB
15 type StationTemplateUrl,
16 type StorageConfiguration,
268a74bb 17 StorageType,
e7aeea18 18 SupervisionUrlDistribution,
83e00df1
JB
19 type UIServerConfiguration,
20 type WorkerConfiguration,
268a74bb
JB
21} from '../types';
22import { WorkerConstants, WorkerProcessType } from '../worker';
7dde0b73 23
268a74bb 24export class Configuration {
a95873d8 25 private static configurationFile = path.join(
51022aa0 26 path.dirname(fileURLToPath(import.meta.url)),
e7aeea18
JB
27 'assets',
28 'config.json'
29 );
10068088 30
1895299d 31 private static configurationFileWatcher: fs.FSWatcher | undefined;
6e0964c8 32 private static configuration: ConfigurationData | null = null;
e57acf6a
JB
33 private static configurationChangeCallback: () => Promise<void>;
34
d5bd1c00
JB
35 private constructor() {
36 // This is intentional
37 }
38
aa7d6d95 39 public static setConfigurationChangeCallback(cb: () => Promise<void>): void {
e57acf6a
JB
40 Configuration.configurationChangeCallback = cb;
41 }
7dde0b73 42
aa7d6d95 43 public static getLogStatisticsInterval(): number | undefined {
e7aeea18
JB
44 Configuration.warnDeprecatedConfigurationKey(
45 'statisticsDisplayInterval',
1895299d 46 undefined,
e7aeea18
JB
47 "Use 'logStatisticsInterval' instead"
48 );
7dde0b73 49 // Read conf
ef4932d8 50 return Utils.hasOwnProp(Configuration.getConfig(), 'logStatisticsInterval')
1895299d 51 ? Configuration.getConfig()?.logStatisticsInterval
25f5a959 52 : Constants.DEFAULT_LOG_STATISTICS_INTERVAL;
72f041bd
JB
53 }
54
aa7d6d95 55 public static getUIServer(): UIServerConfiguration {
ef4932d8 56 if (Utils.hasOwnProp(Configuration.getConfig(), 'uiWebSocketServer')) {
66271092
JB
57 console.error(
58 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration section 'uiWebSocketServer' usage. Use 'uiServer' instead}`
59 );
60 }
675fa8e3 61 let uiServerConfiguration: UIServerConfiguration = {
b803eefa 62 enabled: false,
1f7fa4de 63 type: ApplicationProtocol.WS,
c127bd64 64 options: {
adbddcb4
JB
65 host: Constants.DEFAULT_UI_SERVER_HOST,
66 port: Constants.DEFAULT_UI_SERVER_PORT,
c127bd64 67 },
6a49ad23 68 };
ef4932d8 69 if (Utils.hasOwnProp(Configuration.getConfig(), 'uiServer')) {
598c886d
JB
70 uiServerConfiguration = merge<UIServerConfiguration>(
71 uiServerConfiguration,
72 Configuration.getConfig()?.uiServer
73 );
6a49ad23 74 }
dada83ec 75 if (Utils.isCFEnvironment() === true) {
72092cfc 76 delete uiServerConfiguration.options?.host;
b803eefa
JB
77 uiServerConfiguration.options.port = parseInt(process.env.PORT);
78 }
675fa8e3 79 return uiServerConfiguration;
6a49ad23
JB
80 }
81
aa7d6d95 82 public static getPerformanceStorage(): StorageConfiguration {
e7aeea18 83 Configuration.warnDeprecatedConfigurationKey('URI', 'performanceStorage', "Use 'uri' instead");
6a49ad23
JB
84 let storageConfiguration: StorageConfiguration = {
85 enabled: false,
86 type: StorageType.JSON_FILE,
e7aeea18 87 uri: this.getDefaultPerformanceStorageUri(StorageType.JSON_FILE),
6a49ad23 88 };
ef4932d8 89 if (Utils.hasOwnProp(Configuration.getConfig(), 'performanceStorage')) {
e7aeea18 90 storageConfiguration = {
1ba1e8fb 91 ...storageConfiguration,
1895299d 92 ...Configuration.getConfig()?.performanceStorage,
f682b2dc
JB
93 ...(Configuration.getConfig()?.performanceStorage?.type === StorageType.JSON_FILE &&
94 Configuration.getConfig()?.performanceStorage?.uri && {
e8044a69 95 uri: Configuration.buildPerformanceUriFilePath(
f682b2dc 96 new URL(Configuration.getConfig()?.performanceStorage?.uri).pathname
e8044a69 97 ),
f682b2dc 98 }),
72f041bd 99 };
72f041bd
JB
100 }
101 return storageConfiguration;
7dde0b73
JB
102 }
103
aa7d6d95 104 public static getAutoReconnectMaxRetries(): number | undefined {
e7aeea18
JB
105 Configuration.warnDeprecatedConfigurationKey(
106 'autoReconnectTimeout',
1895299d 107 undefined,
e7aeea18
JB
108 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
109 );
110 Configuration.warnDeprecatedConfigurationKey(
111 'connectionTimeout',
1895299d 112 undefined,
e7aeea18
JB
113 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
114 );
115 Configuration.warnDeprecatedConfigurationKey(
116 'autoReconnectMaxRetries',
1895299d 117 undefined,
e7aeea18
JB
118 'Use it in charging station template instead'
119 );
7dde0b73 120 // Read conf
ef4932d8 121 if (Utils.hasOwnProp(Configuration.getConfig(), 'autoReconnectMaxRetries')) {
1895299d 122 return Configuration.getConfig()?.autoReconnectMaxRetries;
3574dfd3 123 }
7dde0b73
JB
124 }
125
aa7d6d95 126 public static getStationTemplateUrls(): StationTemplateUrl[] | undefined {
e7aeea18
JB
127 Configuration.warnDeprecatedConfigurationKey(
128 'stationTemplateURLs',
1895299d 129 undefined,
e7aeea18
JB
130 "Use 'stationTemplateUrls' instead"
131 );
dada83ec 132 !Utils.isUndefined(Configuration.getConfig()['stationTemplateURLs']) &&
e7aeea18
JB
133 (Configuration.getConfig().stationTemplateUrls = Configuration.getConfig()[
134 'stationTemplateURLs'
617cad0c 135 ] as StationTemplateUrl[]);
7436ee0d
JB
136 Configuration.getConfig().stationTemplateUrls.forEach(
137 (stationTemplateUrl: StationTemplateUrl) => {
138 if (!Utils.isUndefined(stationTemplateUrl['numberOfStation'])) {
139 console.error(
140 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration key 'numberOfStation' usage for template file '${
141 stationTemplateUrl.file
142 }' in 'stationTemplateUrls'. Use 'numberOfStations' instead}`
143 );
144 }
eb3937cb 145 }
7436ee0d 146 );
7dde0b73 147 // Read conf
1895299d 148 return Configuration.getConfig()?.stationTemplateUrls;
7dde0b73
JB
149 }
150
aa7d6d95 151 public static getWorker(): WorkerConfiguration {
e7aeea18 152 Configuration.warnDeprecatedConfigurationKey(
e80bc579 153 'useWorkerPool',
1895299d 154 undefined,
cf2a5d9b
JB
155 "Use 'worker' section to define the type of worker process model instead"
156 );
157 Configuration.warnDeprecatedConfigurationKey(
158 'workerProcess',
1895299d 159 undefined,
cf2a5d9b
JB
160 "Use 'worker' section to define the type of worker process model instead"
161 );
162 Configuration.warnDeprecatedConfigurationKey(
163 'workerStartDelay',
1895299d 164 undefined,
cf2a5d9b
JB
165 "Use 'worker' section to define the worker start delay instead"
166 );
167 Configuration.warnDeprecatedConfigurationKey(
168 'chargingStationsPerWorker',
1895299d 169 undefined,
cf2a5d9b
JB
170 "Use 'worker' section to define the number of element(s) per worker instead"
171 );
172 Configuration.warnDeprecatedConfigurationKey(
173 'elementStartDelay',
1895299d 174 undefined,
cf2a5d9b
JB
175 "Use 'worker' section to define the worker's element start delay instead"
176 );
177 Configuration.warnDeprecatedConfigurationKey(
178 'workerPoolMinSize',
1895299d 179 undefined,
cf2a5d9b 180 "Use 'worker' section to define the worker pool minimum size instead"
e7aeea18 181 );
e7aeea18
JB
182 Configuration.warnDeprecatedConfigurationKey(
183 'workerPoolSize;',
1895299d 184 undefined,
cf2a5d9b 185 "Use 'worker' section to define the worker pool maximum size instead"
e7aeea18 186 );
cf2a5d9b
JB
187 Configuration.warnDeprecatedConfigurationKey(
188 'workerPoolMaxSize;',
1895299d 189 undefined,
cf2a5d9b
JB
190 "Use 'worker' section to define the worker pool maximum size instead"
191 );
192 Configuration.warnDeprecatedConfigurationKey(
193 'workerPoolStrategy;',
1895299d 194 undefined,
cf2a5d9b
JB
195 "Use 'worker' section to define the worker pool strategy instead"
196 );
c127bd64 197 let workerConfiguration: WorkerConfiguration = {
ef4932d8 198 processType: Utils.hasOwnProp(Configuration.getConfig(), 'workerProcess')
1895299d 199 ? Configuration.getConfig()?.workerProcess
721646e9 200 : WorkerProcessType.workerSet,
ef4932d8 201 startDelay: Utils.hasOwnProp(Configuration.getConfig(), 'workerStartDelay')
1895299d 202 ? Configuration.getConfig()?.workerStartDelay
cf2a5d9b 203 : WorkerConstants.DEFAULT_WORKER_START_DELAY,
ef4932d8 204 elementsPerWorker: Utils.hasOwnProp(Configuration.getConfig(), 'chargingStationsPerWorker')
1895299d 205 ? Configuration.getConfig()?.chargingStationsPerWorker
cf2a5d9b 206 : WorkerConstants.DEFAULT_ELEMENTS_PER_WORKER,
ef4932d8 207 elementStartDelay: Utils.hasOwnProp(Configuration.getConfig(), 'elementStartDelay')
1895299d 208 ? Configuration.getConfig()?.elementStartDelay
cf2a5d9b 209 : WorkerConstants.DEFAULT_ELEMENT_START_DELAY,
ef4932d8 210 poolMinSize: Utils.hasOwnProp(Configuration.getConfig(), 'workerPoolMinSize')
1895299d 211 ? Configuration.getConfig()?.workerPoolMinSize
cf2a5d9b 212 : WorkerConstants.DEFAULT_POOL_MIN_SIZE,
ef4932d8 213 poolMaxSize: Utils.hasOwnProp(Configuration.getConfig(), 'workerPoolMaxSize')
1895299d 214 ? Configuration.getConfig()?.workerPoolMaxSize
cf2a5d9b 215 : WorkerConstants.DEFAULT_POOL_MAX_SIZE,
156cab2c 216 poolStrategy:
1895299d 217 Configuration.getConfig()?.workerPoolStrategy ?? WorkerChoiceStrategies.ROUND_ROBIN,
cf2a5d9b 218 };
ef4932d8 219 if (Utils.hasOwnProp(Configuration.getConfig(), 'worker')) {
1895299d 220 workerConfiguration = { ...workerConfiguration, ...Configuration.getConfig()?.worker };
cf2a5d9b
JB
221 }
222 return workerConfiguration;
3d2ff9e4
J
223 }
224
aa7d6d95
JB
225 public static workerPoolInUse(): boolean {
226 return [WorkerProcessType.dynamicPool, WorkerProcessType.staticPool].includes(
227 Configuration.getWorker().processType
228 );
229 }
230
231 public static workerDynamicPoolInUse(): boolean {
232 return Configuration.getWorker().processType === WorkerProcessType.dynamicPool;
233 }
234
235 public static getLogConsole(): boolean | undefined {
1895299d
JB
236 Configuration.warnDeprecatedConfigurationKey(
237 'consoleLog',
238 undefined,
239 "Use 'logConsole' instead"
240 );
ef4932d8 241 return Utils.hasOwnProp(Configuration.getConfig(), 'logConsole')
1895299d 242 ? Configuration.getConfig()?.logConsole
e7aeea18 243 : false;
7dde0b73
JB
244 }
245
aa7d6d95 246 public static getLogFormat(): string | undefined {
ef4932d8 247 return Utils.hasOwnProp(Configuration.getConfig(), 'logFormat')
1895299d 248 ? Configuration.getConfig()?.logFormat
e7aeea18 249 : 'simple';
027b409a
JB
250 }
251
aa7d6d95 252 public static getLogRotate(): boolean | undefined {
ef4932d8 253 return Utils.hasOwnProp(Configuration.getConfig(), 'logRotate')
1895299d 254 ? Configuration.getConfig()?.logRotate
e7aeea18 255 : true;
6bf6769e
JB
256 }
257
aa7d6d95 258 public static getLogMaxFiles(): number | string | false | undefined {
9988696d 259 return (
ef4932d8 260 Utils.hasOwnProp(Configuration.getConfig(), 'logMaxFiles') &&
1895299d 261 Configuration.getConfig()?.logMaxFiles
9988696d
JB
262 );
263 }
264
aa7d6d95 265 public static getLogMaxSize(): number | string | false | undefined {
9988696d 266 return (
ef4932d8 267 Utils.hasOwnProp(Configuration.getConfig(), 'logMaxFiles') &&
1895299d 268 Configuration.getConfig()?.logMaxSize
9988696d 269 );
6bf6769e
JB
270 }
271
aa7d6d95 272 public static getLogLevel(): string | undefined {
ef4932d8 273 return Utils.hasOwnProp(Configuration.getConfig(), 'logLevel')
1895299d 274 ? Configuration.getConfig()?.logLevel?.toLowerCase()
e7aeea18 275 : 'info';
2e6f5966
JB
276 }
277
aa7d6d95 278 public static getLogFile(): string | undefined {
ef4932d8 279 return Utils.hasOwnProp(Configuration.getConfig(), 'logFile')
1895299d 280 ? Configuration.getConfig()?.logFile
e7aeea18 281 : 'combined.log';
7dde0b73
JB
282 }
283
aa7d6d95 284 public static getLogErrorFile(): string | undefined {
1895299d
JB
285 Configuration.warnDeprecatedConfigurationKey(
286 'errorFile',
287 undefined,
288 "Use 'logErrorFile' instead"
289 );
ef4932d8 290 return Utils.hasOwnProp(Configuration.getConfig(), 'logErrorFile')
1895299d 291 ? Configuration.getConfig()?.logErrorFile
e7aeea18 292 : 'error.log';
7dde0b73
JB
293 }
294
aa7d6d95 295 public static getSupervisionUrls(): string | string[] | undefined {
e7aeea18
JB
296 Configuration.warnDeprecatedConfigurationKey(
297 'supervisionURLs',
1895299d 298 undefined,
e7aeea18
JB
299 "Use 'supervisionUrls' instead"
300 );
dada83ec 301 !Utils.isUndefined(Configuration.getConfig()['supervisionURLs']) &&
1895299d
JB
302 (Configuration.getConfig().supervisionUrls = Configuration.getConfig()['supervisionURLs'] as
303 | string
304 | string[]);
7dde0b73 305 // Read conf
1895299d 306 return Configuration.getConfig()?.supervisionUrls;
7dde0b73
JB
307 }
308
aa7d6d95 309 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution | undefined {
e7aeea18
JB
310 Configuration.warnDeprecatedConfigurationKey(
311 'distributeStationToTenantEqually',
1895299d 312 undefined,
e7aeea18
JB
313 "Use 'supervisionUrlDistribution' instead"
314 );
315 Configuration.warnDeprecatedConfigurationKey(
316 'distributeStationsToTenantsEqually',
1895299d 317 undefined,
e7aeea18
JB
318 "Use 'supervisionUrlDistribution' instead"
319 );
ef4932d8 320 return Utils.hasOwnProp(Configuration.getConfig(), 'supervisionUrlDistribution')
1895299d 321 ? Configuration.getConfig()?.supervisionUrlDistribution
e7aeea18 322 : SupervisionUrlDistribution.ROUND_ROBIN;
7dde0b73 323 }
eb3937cb 324
8b7072dc 325 private static logPrefix = (): string => {
14ecae6a 326 return `${new Date().toLocaleString()} Simulator configuration |`;
8b7072dc 327 };
23132a44 328
e7aeea18
JB
329 private static warnDeprecatedConfigurationKey(
330 key: string,
331 sectionName?: string,
332 logMsgToAppend = ''
333 ) {
e7aeea18
JB
334 if (
335 sectionName &&
dada83ec
JB
336 !Utils.isUndefined(Configuration.getConfig()[sectionName]) &&
337 !Utils.isUndefined((Configuration.getConfig()[sectionName] as Record<string, unknown>)[key])
e7aeea18
JB
338 ) {
339 console.error(
340 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration key '${key}' usage in section '${sectionName}'${
e302df1d 341 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
e7aeea18
JB
342 }}`
343 );
dada83ec 344 } else if (!Utils.isUndefined(Configuration.getConfig()[key])) {
e7aeea18
JB
345 console.error(
346 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration key '${key}' usage${
e302df1d 347 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
e7aeea18
JB
348 }}`
349 );
eb3937cb
JB
350 }
351 }
352
353 // Read the config file
1895299d 354 private static getConfig(): ConfigurationData | null {
eb3937cb 355 if (!Configuration.configuration) {
23132a44 356 try {
e7aeea18 357 Configuration.configuration = JSON.parse(
a95873d8 358 fs.readFileSync(Configuration.configurationFile, 'utf8')
e7aeea18 359 ) as ConfigurationData;
23132a44 360 } catch (error) {
69074173 361 Configuration.handleFileException(
a95873d8 362 Configuration.configurationFile,
7164966d
JB
363 FileType.Configuration,
364 error as NodeJS.ErrnoException,
69074173 365 Configuration.logPrefix()
e7aeea18 366 );
23132a44 367 }
ded13d97
JB
368 if (!Configuration.configurationFileWatcher) {
369 Configuration.configurationFileWatcher = Configuration.getConfigurationFileWatcher();
370 }
eb3937cb
JB
371 }
372 return Configuration.configuration;
373 }
963ee397 374
1895299d 375 private static getConfigurationFileWatcher(): fs.FSWatcher | undefined {
23132a44 376 try {
a95873d8 377 return fs.watch(Configuration.configurationFile, (event, filename): void => {
8d54dcc0 378 if (filename?.trim().length > 0 && event === 'change') {
3ec10737
JB
379 // Nullify to force configuration file reading
380 Configuration.configuration = null;
dada83ec 381 if (!Utils.isUndefined(Configuration.configurationChangeCallback)) {
72092cfc 382 Configuration.configurationChangeCallback().catch((error) => {
dcaf96dc
JB
383 throw typeof error === 'string' ? new Error(error) : error;
384 });
3ec10737 385 }
23132a44
JB
386 }
387 });
388 } catch (error) {
69074173 389 Configuration.handleFileException(
a95873d8 390 Configuration.configurationFile,
7164966d
JB
391 FileType.Configuration,
392 error as NodeJS.ErrnoException,
69074173 393 Configuration.logPrefix()
e7aeea18 394 );
23132a44 395 }
ded13d97
JB
396 }
397
69074173
JB
398 private static handleFileException(
399 file: string,
400 fileType: FileType,
401 error: NodeJS.ErrnoException,
402 logPrefix: string
403 ): void {
404 const prefix = Utils.isNotEmptyString(logPrefix) ? `${logPrefix} ` : '';
405 let logMsg: string;
406 switch (error.code) {
407 case 'ENOENT':
408 logMsg = `${fileType} file ${file} not found:`;
409 break;
410 case 'EEXIST':
411 logMsg = `${fileType} file ${file} already exists:`;
412 break;
413 case 'EACCES':
414 logMsg = `${fileType} file ${file} access denied:`;
415 break;
7b5dbe91
JB
416 case 'EPERM':
417 logMsg = `${fileType} file ${file} permission denied:`;
418 break;
69074173
JB
419 default:
420 logMsg = `${fileType} file ${file} error:`;
421 }
7b5dbe91
JB
422 console.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
423 throw error;
69074173
JB
424 }
425
1f5df42a 426 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
d5603918
JB
427 switch (storageType) {
428 case StorageType.JSON_FILE:
e8044a69 429 return Configuration.buildPerformanceUriFilePath(
e7aeea18 430 Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME
e8044a69 431 );
d5603918 432 case StorageType.SQLITE:
e8044a69 433 return Configuration.buildPerformanceUriFilePath(
5bcb75d4 434 `${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`
e8044a69 435 );
d5603918
JB
436 default:
437 throw new Error(`Performance storage URI is mandatory with storage type '${storageType}'`);
438 }
439 }
e8044a69
JB
440
441 private static buildPerformanceUriFilePath(file: string) {
442 return `file://${path.join(
51022aa0 443 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
e8044a69
JB
444 file
445 )}`;
446 }
7dde0b73 447}