build(simulator): don't preserve modules
[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
e57acf6a
JB
39 static setConfigurationChangeCallback(cb: () => Promise<void>): void {
40 Configuration.configurationChangeCallback = cb;
41 }
7dde0b73 42
1895299d 43 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
675fa8e3 55 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
72f041bd 82 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
1895299d 104 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
1895299d 126 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
cf2a5d9b 151 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
1895299d
JB
225 static getLogConsole(): boolean | undefined {
226 Configuration.warnDeprecatedConfigurationKey(
227 'consoleLog',
228 undefined,
229 "Use 'logConsole' instead"
230 );
ef4932d8 231 return Utils.hasOwnProp(Configuration.getConfig(), 'logConsole')
1895299d 232 ? Configuration.getConfig()?.logConsole
e7aeea18 233 : false;
7dde0b73
JB
234 }
235
1895299d 236 static getLogFormat(): string | undefined {
ef4932d8 237 return Utils.hasOwnProp(Configuration.getConfig(), 'logFormat')
1895299d 238 ? Configuration.getConfig()?.logFormat
e7aeea18 239 : 'simple';
027b409a
JB
240 }
241
1895299d 242 static getLogRotate(): boolean | undefined {
ef4932d8 243 return Utils.hasOwnProp(Configuration.getConfig(), 'logRotate')
1895299d 244 ? Configuration.getConfig()?.logRotate
e7aeea18 245 : true;
6bf6769e
JB
246 }
247
1895299d 248 static getLogMaxFiles(): number | string | false | undefined {
9988696d 249 return (
ef4932d8 250 Utils.hasOwnProp(Configuration.getConfig(), 'logMaxFiles') &&
1895299d 251 Configuration.getConfig()?.logMaxFiles
9988696d
JB
252 );
253 }
254
1895299d 255 static getLogMaxSize(): number | string | false | undefined {
9988696d 256 return (
ef4932d8 257 Utils.hasOwnProp(Configuration.getConfig(), 'logMaxFiles') &&
1895299d 258 Configuration.getConfig()?.logMaxSize
9988696d 259 );
6bf6769e
JB
260 }
261
1895299d 262 static getLogLevel(): string | undefined {
ef4932d8 263 return Utils.hasOwnProp(Configuration.getConfig(), 'logLevel')
1895299d 264 ? Configuration.getConfig()?.logLevel?.toLowerCase()
e7aeea18 265 : 'info';
2e6f5966
JB
266 }
267
1895299d 268 static getLogFile(): string | undefined {
ef4932d8 269 return Utils.hasOwnProp(Configuration.getConfig(), 'logFile')
1895299d 270 ? Configuration.getConfig()?.logFile
e7aeea18 271 : 'combined.log';
7dde0b73
JB
272 }
273
1895299d
JB
274 static getLogErrorFile(): string | undefined {
275 Configuration.warnDeprecatedConfigurationKey(
276 'errorFile',
277 undefined,
278 "Use 'logErrorFile' instead"
279 );
ef4932d8 280 return Utils.hasOwnProp(Configuration.getConfig(), 'logErrorFile')
1895299d 281 ? Configuration.getConfig()?.logErrorFile
e7aeea18 282 : 'error.log';
7dde0b73
JB
283 }
284
1895299d 285 static getSupervisionUrls(): string | string[] | undefined {
e7aeea18
JB
286 Configuration.warnDeprecatedConfigurationKey(
287 'supervisionURLs',
1895299d 288 undefined,
e7aeea18
JB
289 "Use 'supervisionUrls' instead"
290 );
dada83ec 291 !Utils.isUndefined(Configuration.getConfig()['supervisionURLs']) &&
1895299d
JB
292 (Configuration.getConfig().supervisionUrls = Configuration.getConfig()['supervisionURLs'] as
293 | string
294 | string[]);
7dde0b73 295 // Read conf
1895299d 296 return Configuration.getConfig()?.supervisionUrls;
7dde0b73
JB
297 }
298
1895299d 299 static getSupervisionUrlDistribution(): SupervisionUrlDistribution | undefined {
e7aeea18
JB
300 Configuration.warnDeprecatedConfigurationKey(
301 'distributeStationToTenantEqually',
1895299d 302 undefined,
e7aeea18
JB
303 "Use 'supervisionUrlDistribution' instead"
304 );
305 Configuration.warnDeprecatedConfigurationKey(
306 'distributeStationsToTenantsEqually',
1895299d 307 undefined,
e7aeea18
JB
308 "Use 'supervisionUrlDistribution' instead"
309 );
ef4932d8 310 return Utils.hasOwnProp(Configuration.getConfig(), 'supervisionUrlDistribution')
1895299d 311 ? Configuration.getConfig()?.supervisionUrlDistribution
e7aeea18 312 : SupervisionUrlDistribution.ROUND_ROBIN;
7dde0b73 313 }
eb3937cb 314
8b7072dc 315 private static logPrefix = (): string => {
14ecae6a 316 return `${new Date().toLocaleString()} Simulator configuration |`;
8b7072dc 317 };
23132a44 318
e7aeea18
JB
319 private static warnDeprecatedConfigurationKey(
320 key: string,
321 sectionName?: string,
322 logMsgToAppend = ''
323 ) {
e7aeea18
JB
324 if (
325 sectionName &&
dada83ec
JB
326 !Utils.isUndefined(Configuration.getConfig()[sectionName]) &&
327 !Utils.isUndefined((Configuration.getConfig()[sectionName] as Record<string, unknown>)[key])
e7aeea18
JB
328 ) {
329 console.error(
330 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration key '${key}' usage in section '${sectionName}'${
e302df1d 331 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
e7aeea18
JB
332 }}`
333 );
dada83ec 334 } else if (!Utils.isUndefined(Configuration.getConfig()[key])) {
e7aeea18
JB
335 console.error(
336 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration key '${key}' usage${
e302df1d 337 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
e7aeea18
JB
338 }}`
339 );
eb3937cb
JB
340 }
341 }
342
343 // Read the config file
1895299d 344 private static getConfig(): ConfigurationData | null {
eb3937cb 345 if (!Configuration.configuration) {
23132a44 346 try {
e7aeea18 347 Configuration.configuration = JSON.parse(
a95873d8 348 fs.readFileSync(Configuration.configurationFile, 'utf8')
e7aeea18 349 ) as ConfigurationData;
23132a44 350 } catch (error) {
69074173 351 Configuration.handleFileException(
a95873d8 352 Configuration.configurationFile,
7164966d
JB
353 FileType.Configuration,
354 error as NodeJS.ErrnoException,
69074173 355 Configuration.logPrefix()
e7aeea18 356 );
23132a44 357 }
ded13d97
JB
358 if (!Configuration.configurationFileWatcher) {
359 Configuration.configurationFileWatcher = Configuration.getConfigurationFileWatcher();
360 }
eb3937cb
JB
361 }
362 return Configuration.configuration;
363 }
963ee397 364
1895299d 365 private static getConfigurationFileWatcher(): fs.FSWatcher | undefined {
23132a44 366 try {
a95873d8 367 return fs.watch(Configuration.configurationFile, (event, filename): void => {
8d54dcc0 368 if (filename?.trim().length > 0 && event === 'change') {
3ec10737
JB
369 // Nullify to force configuration file reading
370 Configuration.configuration = null;
dada83ec 371 if (!Utils.isUndefined(Configuration.configurationChangeCallback)) {
72092cfc 372 Configuration.configurationChangeCallback().catch((error) => {
dcaf96dc
JB
373 throw typeof error === 'string' ? new Error(error) : error;
374 });
3ec10737 375 }
23132a44
JB
376 }
377 });
378 } catch (error) {
69074173 379 Configuration.handleFileException(
a95873d8 380 Configuration.configurationFile,
7164966d
JB
381 FileType.Configuration,
382 error as NodeJS.ErrnoException,
69074173 383 Configuration.logPrefix()
e7aeea18 384 );
23132a44 385 }
ded13d97
JB
386 }
387
69074173
JB
388 private static handleFileException(
389 file: string,
390 fileType: FileType,
391 error: NodeJS.ErrnoException,
392 logPrefix: string
393 ): void {
394 const prefix = Utils.isNotEmptyString(logPrefix) ? `${logPrefix} ` : '';
395 let logMsg: string;
396 switch (error.code) {
397 case 'ENOENT':
398 logMsg = `${fileType} file ${file} not found:`;
399 break;
400 case 'EEXIST':
401 logMsg = `${fileType} file ${file} already exists:`;
402 break;
403 case 'EACCES':
404 logMsg = `${fileType} file ${file} access denied:`;
405 break;
406 default:
407 logMsg = `${fileType} file ${file} error:`;
408 }
409 console.warn(`${chalk.green(prefix)}${chalk.yellow(`${logMsg} `)}`, error);
410 }
411
1f5df42a 412 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
d5603918
JB
413 switch (storageType) {
414 case StorageType.JSON_FILE:
e8044a69 415 return Configuration.buildPerformanceUriFilePath(
e7aeea18 416 Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME
e8044a69 417 );
d5603918 418 case StorageType.SQLITE:
e8044a69 419 return Configuration.buildPerformanceUriFilePath(
5bcb75d4 420 `${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`
e8044a69 421 );
d5603918
JB
422 default:
423 throw new Error(`Performance storage URI is mandatory with storage type '${storageType}'`);
424 }
425 }
e8044a69
JB
426
427 private static buildPerformanceUriFilePath(file: string) {
428 return `file://${path.join(
51022aa0 429 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
e8044a69
JB
430 file
431 )}`;
432 }
7dde0b73 433}