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