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