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