refactor: revert internal exports in utils
[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 { FileUtils } from './FileUtils';
11 import { Utils } from './Utils';
12 import {
13 ApplicationProtocol,
14 type ConfigurationData,
15 FileType,
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 Utils.hasOwnProp(Configuration.getConfig(), 'logStatisticsInterval')
52 ? Configuration.getConfig()?.logStatisticsInterval
53 : Constants.DEFAULT_LOG_STATISTICS_INTERVAL;
54 }
55
56 static getUIServer(): UIServerConfiguration {
57 if (Utils.hasOwnProp(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 (Utils.hasOwnProp(Configuration.getConfig(), 'uiServer')) {
71 uiServerConfiguration = merge<UIServerConfiguration>(
72 uiServerConfiguration,
73 Configuration.getConfig()?.uiServer
74 );
75 }
76 if (Utils.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 (Utils.hasOwnProp(Configuration.getConfig(), 'performanceStorage')) {
91 storageConfiguration = {
92 ...storageConfiguration,
93 ...Configuration.getConfig()?.performanceStorage,
94 ...(Configuration.getConfig()?.performanceStorage?.type === StorageType.JSON_FILE &&
95 Configuration.getConfig()?.performanceStorage?.uri && {
96 uri: Configuration.buildPerformanceUriFilePath(
97 new URL(Configuration.getConfig()?.performanceStorage?.uri).pathname
98 ),
99 }),
100 };
101 }
102 return storageConfiguration;
103 }
104
105 static getAutoReconnectMaxRetries(): number | undefined {
106 Configuration.warnDeprecatedConfigurationKey(
107 'autoReconnectTimeout',
108 undefined,
109 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
110 );
111 Configuration.warnDeprecatedConfigurationKey(
112 'connectionTimeout',
113 undefined,
114 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
115 );
116 Configuration.warnDeprecatedConfigurationKey(
117 'autoReconnectMaxRetries',
118 undefined,
119 'Use it in charging station template instead'
120 );
121 // Read conf
122 if (Utils.hasOwnProp(Configuration.getConfig(), 'autoReconnectMaxRetries')) {
123 return Configuration.getConfig()?.autoReconnectMaxRetries;
124 }
125 }
126
127 static getStationTemplateUrls(): StationTemplateUrl[] | undefined {
128 Configuration.warnDeprecatedConfigurationKey(
129 'stationTemplateURLs',
130 undefined,
131 "Use 'stationTemplateUrls' instead"
132 );
133 !Utils.isUndefined(Configuration.getConfig()['stationTemplateURLs']) &&
134 (Configuration.getConfig().stationTemplateUrls = Configuration.getConfig()[
135 'stationTemplateURLs'
136 ] as StationTemplateUrl[]);
137 Configuration.getConfig().stationTemplateUrls.forEach(
138 (stationTemplateUrl: StationTemplateUrl) => {
139 if (!Utils.isUndefined(stationTemplateUrl['numberOfStation'])) {
140 console.error(
141 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration key 'numberOfStation' usage for template file '${
142 stationTemplateUrl.file
143 }' in 'stationTemplateUrls'. Use 'numberOfStations' instead}`
144 );
145 }
146 }
147 );
148 // Read conf
149 return Configuration.getConfig()?.stationTemplateUrls;
150 }
151
152 static getWorker(): WorkerConfiguration {
153 Configuration.warnDeprecatedConfigurationKey(
154 'useWorkerPool',
155 undefined,
156 "Use 'worker' section to define the type of worker process model instead"
157 );
158 Configuration.warnDeprecatedConfigurationKey(
159 'workerProcess',
160 undefined,
161 "Use 'worker' section to define the type of worker process model instead"
162 );
163 Configuration.warnDeprecatedConfigurationKey(
164 'workerStartDelay',
165 undefined,
166 "Use 'worker' section to define the worker start delay instead"
167 );
168 Configuration.warnDeprecatedConfigurationKey(
169 'chargingStationsPerWorker',
170 undefined,
171 "Use 'worker' section to define the number of element(s) per worker instead"
172 );
173 Configuration.warnDeprecatedConfigurationKey(
174 'elementStartDelay',
175 undefined,
176 "Use 'worker' section to define the worker's element start delay instead"
177 );
178 Configuration.warnDeprecatedConfigurationKey(
179 'workerPoolMinSize',
180 undefined,
181 "Use 'worker' section to define the worker pool minimum size instead"
182 );
183 Configuration.warnDeprecatedConfigurationKey(
184 'workerPoolSize;',
185 undefined,
186 "Use 'worker' section to define the worker pool maximum size instead"
187 );
188 Configuration.warnDeprecatedConfigurationKey(
189 'workerPoolMaxSize;',
190 undefined,
191 "Use 'worker' section to define the worker pool maximum size instead"
192 );
193 Configuration.warnDeprecatedConfigurationKey(
194 'workerPoolStrategy;',
195 undefined,
196 "Use 'worker' section to define the worker pool strategy instead"
197 );
198 let workerConfiguration: WorkerConfiguration = {
199 processType: Utils.hasOwnProp(Configuration.getConfig(), 'workerProcess')
200 ? Configuration.getConfig()?.workerProcess
201 : WorkerProcessType.workerSet,
202 startDelay: Utils.hasOwnProp(Configuration.getConfig(), 'workerStartDelay')
203 ? Configuration.getConfig()?.workerStartDelay
204 : WorkerConstants.DEFAULT_WORKER_START_DELAY,
205 elementsPerWorker: Utils.hasOwnProp(Configuration.getConfig(), 'chargingStationsPerWorker')
206 ? Configuration.getConfig()?.chargingStationsPerWorker
207 : WorkerConstants.DEFAULT_ELEMENTS_PER_WORKER,
208 elementStartDelay: Utils.hasOwnProp(Configuration.getConfig(), 'elementStartDelay')
209 ? Configuration.getConfig()?.elementStartDelay
210 : WorkerConstants.DEFAULT_ELEMENT_START_DELAY,
211 poolMinSize: Utils.hasOwnProp(Configuration.getConfig(), 'workerPoolMinSize')
212 ? Configuration.getConfig()?.workerPoolMinSize
213 : WorkerConstants.DEFAULT_POOL_MIN_SIZE,
214 poolMaxSize: Utils.hasOwnProp(Configuration.getConfig(), 'workerPoolMaxSize')
215 ? Configuration.getConfig()?.workerPoolMaxSize
216 : WorkerConstants.DEFAULT_POOL_MAX_SIZE,
217 poolStrategy:
218 Configuration.getConfig()?.workerPoolStrategy ?? WorkerChoiceStrategies.ROUND_ROBIN,
219 };
220 if (Utils.hasOwnProp(Configuration.getConfig(), 'worker')) {
221 workerConfiguration = { ...workerConfiguration, ...Configuration.getConfig()?.worker };
222 }
223 return workerConfiguration;
224 }
225
226 static getLogConsole(): boolean | undefined {
227 Configuration.warnDeprecatedConfigurationKey(
228 'consoleLog',
229 undefined,
230 "Use 'logConsole' instead"
231 );
232 return Utils.hasOwnProp(Configuration.getConfig(), 'logConsole')
233 ? Configuration.getConfig()?.logConsole
234 : false;
235 }
236
237 static getLogFormat(): string | undefined {
238 return Utils.hasOwnProp(Configuration.getConfig(), 'logFormat')
239 ? Configuration.getConfig()?.logFormat
240 : 'simple';
241 }
242
243 static getLogRotate(): boolean | undefined {
244 return Utils.hasOwnProp(Configuration.getConfig(), 'logRotate')
245 ? Configuration.getConfig()?.logRotate
246 : true;
247 }
248
249 static getLogMaxFiles(): number | string | false | undefined {
250 return (
251 Utils.hasOwnProp(Configuration.getConfig(), 'logMaxFiles') &&
252 Configuration.getConfig()?.logMaxFiles
253 );
254 }
255
256 static getLogMaxSize(): number | string | false | undefined {
257 return (
258 Utils.hasOwnProp(Configuration.getConfig(), 'logMaxFiles') &&
259 Configuration.getConfig()?.logMaxSize
260 );
261 }
262
263 static getLogLevel(): string | undefined {
264 return Utils.hasOwnProp(Configuration.getConfig(), 'logLevel')
265 ? Configuration.getConfig()?.logLevel?.toLowerCase()
266 : 'info';
267 }
268
269 static getLogFile(): string | undefined {
270 return Utils.hasOwnProp(Configuration.getConfig(), 'logFile')
271 ? Configuration.getConfig()?.logFile
272 : 'combined.log';
273 }
274
275 static getLogErrorFile(): string | undefined {
276 Configuration.warnDeprecatedConfigurationKey(
277 'errorFile',
278 undefined,
279 "Use 'logErrorFile' instead"
280 );
281 return Utils.hasOwnProp(Configuration.getConfig(), 'logErrorFile')
282 ? Configuration.getConfig()?.logErrorFile
283 : 'error.log';
284 }
285
286 static getSupervisionUrls(): string | string[] | undefined {
287 Configuration.warnDeprecatedConfigurationKey(
288 'supervisionURLs',
289 undefined,
290 "Use 'supervisionUrls' instead"
291 );
292 !Utils.isUndefined(Configuration.getConfig()['supervisionURLs']) &&
293 (Configuration.getConfig().supervisionUrls = Configuration.getConfig()['supervisionURLs'] as
294 | string
295 | string[]);
296 // Read conf
297 return Configuration.getConfig()?.supervisionUrls;
298 }
299
300 static getSupervisionUrlDistribution(): SupervisionUrlDistribution | undefined {
301 Configuration.warnDeprecatedConfigurationKey(
302 'distributeStationToTenantEqually',
303 undefined,
304 "Use 'supervisionUrlDistribution' instead"
305 );
306 Configuration.warnDeprecatedConfigurationKey(
307 'distributeStationsToTenantsEqually',
308 undefined,
309 "Use 'supervisionUrlDistribution' instead"
310 );
311 return Utils.hasOwnProp(Configuration.getConfig(), 'supervisionUrlDistribution')
312 ? Configuration.getConfig()?.supervisionUrlDistribution
313 : SupervisionUrlDistribution.ROUND_ROBIN;
314 }
315
316 private static logPrefix = (): string => {
317 return `${new Date().toLocaleString()} Simulator configuration |`;
318 };
319
320 private static warnDeprecatedConfigurationKey(
321 key: string,
322 sectionName?: string,
323 logMsgToAppend = ''
324 ) {
325 if (
326 sectionName &&
327 !Utils.isUndefined(Configuration.getConfig()[sectionName]) &&
328 !Utils.isUndefined((Configuration.getConfig()[sectionName] as Record<string, unknown>)[key])
329 ) {
330 console.error(
331 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration key '${key}' usage in section '${sectionName}'${
332 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
333 }}`
334 );
335 } else if (!Utils.isUndefined(Configuration.getConfig()[key])) {
336 console.error(
337 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration key '${key}' usage${
338 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
339 }}`
340 );
341 }
342 }
343
344 // Read the config file
345 private static getConfig(): ConfigurationData | null {
346 if (!Configuration.configuration) {
347 try {
348 Configuration.configuration = JSON.parse(
349 fs.readFileSync(Configuration.configurationFile, 'utf8')
350 ) as ConfigurationData;
351 } catch (error) {
352 FileUtils.handleFileException(
353 Configuration.configurationFile,
354 FileType.Configuration,
355 error as NodeJS.ErrnoException,
356 Configuration.logPrefix(),
357 { consoleOut: true }
358 );
359 }
360 if (!Configuration.configurationFileWatcher) {
361 Configuration.configurationFileWatcher = Configuration.getConfigurationFileWatcher();
362 }
363 }
364 return Configuration.configuration;
365 }
366
367 private static getConfigurationFileWatcher(): fs.FSWatcher | undefined {
368 try {
369 return fs.watch(Configuration.configurationFile, (event, filename): void => {
370 if (filename?.trim().length > 0 && event === 'change') {
371 // Nullify to force configuration file reading
372 Configuration.configuration = null;
373 if (!Utils.isUndefined(Configuration.configurationChangeCallback)) {
374 Configuration.configurationChangeCallback().catch((error) => {
375 throw typeof error === 'string' ? new Error(error) : error;
376 });
377 }
378 }
379 });
380 } catch (error) {
381 FileUtils.handleFileException(
382 Configuration.configurationFile,
383 FileType.Configuration,
384 error as NodeJS.ErrnoException,
385 Configuration.logPrefix(),
386 { consoleOut: true }
387 );
388 }
389 }
390
391 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
392 switch (storageType) {
393 case StorageType.JSON_FILE:
394 return Configuration.buildPerformanceUriFilePath(
395 Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME
396 );
397 case StorageType.SQLITE:
398 return Configuration.buildPerformanceUriFilePath(
399 `${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`
400 );
401 default:
402 throw new Error(`Performance storage URI is mandatory with storage type '${storageType}'`);
403 }
404 }
405
406 private static buildPerformanceUriFilePath(file: string) {
407 return `file://${path.join(
408 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../'),
409 file
410 )}`;
411 }
412 }