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