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