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