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