2 import path from
'path';
3 import { fileURLToPath
} from
'url';
5 import chalk from
'chalk';
7 import ConfigurationData
, {
10 SupervisionUrlDistribution
,
11 UIServerConfiguration
,
13 } from
'../types/ConfigurationData';
14 import type { EmptyObject
} from
'../types/EmptyObject';
15 import type { HandleErrorParams
} from
'../types/Error';
16 import { FileType
} from
'../types/FileType';
17 import { StorageType
} from
'../types/Storage';
18 import { ApplicationProtocol
} from
'../types/UIProtocol';
19 import { WorkerProcessType
} from
'../types/Worker';
20 import WorkerConstants from
'../worker/WorkerConstants';
21 import Constants from
'./Constants';
23 export default class Configuration
{
24 private static configurationFile
= path
.join(
25 path
.resolve(path
.dirname(fileURLToPath(import.meta
.url
)), '../'),
30 private static configurationFileWatcher
: fs
.FSWatcher
;
31 private static configuration
: ConfigurationData
| null = null;
32 private static configurationChangeCallback
: () => Promise
<void>;
34 private constructor() {
35 // This is intentional
38 static setConfigurationChangeCallback(cb
: () => Promise
<void>): void {
39 Configuration
.configurationChangeCallback
= cb
;
42 static getLogStatisticsInterval(): number {
43 Configuration
.warnDeprecatedConfigurationKey(
44 'statisticsDisplayInterval',
46 "Use 'logStatisticsInterval' instead"
49 return Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'logStatisticsInterval')
50 ? Configuration
.getConfig().logStatisticsInterval
51 : Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
;
54 static getUIServer(): UIServerConfiguration
{
55 if (Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'uiWebSocketServer')) {
57 chalk
`{green ${Configuration.logPrefix()}} {red Deprecated configuration section 'uiWebSocketServer' usage. Use 'uiServer' instead}`
60 let uiServerConfiguration
: UIServerConfiguration
= {
62 type: ApplicationProtocol
.WS
,
64 host
: Constants
.DEFAULT_UI_WEBSOCKET_SERVER_HOST
,
65 port
: Constants
.DEFAULT_UI_WEBSOCKET_SERVER_PORT
,
68 if (Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'uiServer')) {
69 uiServerConfiguration
= Configuration
.deepMerge(
70 uiServerConfiguration
,
71 Configuration
.getConfig().uiServer
74 return uiServerConfiguration
;
77 static getPerformanceStorage(): StorageConfiguration
{
78 Configuration
.warnDeprecatedConfigurationKey('URI', 'performanceStorage', "Use 'uri' instead");
79 let storageConfiguration
: StorageConfiguration
= {
81 type: StorageType
.JSON_FILE
,
82 uri
: this.getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
84 if (Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'performanceStorage')) {
85 storageConfiguration
= {
86 ...storageConfiguration
,
87 ...Configuration
.getConfig().performanceStorage
,
90 return storageConfiguration
;
93 static getAutoReconnectMaxRetries(): number {
94 Configuration
.warnDeprecatedConfigurationKey(
95 'autoReconnectTimeout',
97 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
99 Configuration
.warnDeprecatedConfigurationKey(
102 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
104 Configuration
.warnDeprecatedConfigurationKey(
105 'autoReconnectMaxRetries',
107 'Use it in charging station template instead'
110 if (Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'autoReconnectMaxRetries')) {
111 return Configuration
.getConfig().autoReconnectMaxRetries
;
115 static getStationTemplateUrls(): StationTemplateUrl
[] {
116 Configuration
.warnDeprecatedConfigurationKey(
117 'stationTemplateURLs',
119 "Use 'stationTemplateUrls' instead"
121 !Configuration
.isUndefined(Configuration
.getConfig()['stationTemplateURLs']) &&
122 (Configuration
.getConfig().stationTemplateUrls
= Configuration
.getConfig()[
123 'stationTemplateURLs'
124 ] as StationTemplateUrl
[]);
125 Configuration
.getConfig().stationTemplateUrls
.forEach((stationUrl
: StationTemplateUrl
) => {
126 if (!Configuration
.isUndefined(stationUrl
['numberOfStation'])) {
128 chalk
`{green ${Configuration.logPrefix()}} {red Deprecated configuration key 'numberOfStation' usage for template file '${
130 }' in 'stationTemplateUrls'. Use 'numberOfStations' instead}`
135 return Configuration
.getConfig().stationTemplateUrls
;
138 static getWorker(): WorkerConfiguration
{
139 Configuration
.warnDeprecatedConfigurationKey(
142 "Use 'worker' section to define the type of worker process model instead"
144 Configuration
.warnDeprecatedConfigurationKey(
147 "Use 'worker' section to define the type of worker process model instead"
149 Configuration
.warnDeprecatedConfigurationKey(
152 "Use 'worker' section to define the worker start delay instead"
154 Configuration
.warnDeprecatedConfigurationKey(
155 'chargingStationsPerWorker',
157 "Use 'worker' section to define the number of element(s) per worker instead"
159 Configuration
.warnDeprecatedConfigurationKey(
162 "Use 'worker' section to define the worker's element start delay instead"
164 Configuration
.warnDeprecatedConfigurationKey(
167 "Use 'worker' section to define the worker pool minimum size instead"
169 Configuration
.warnDeprecatedConfigurationKey(
172 "Use 'worker' section to define the worker pool maximum size instead"
174 Configuration
.warnDeprecatedConfigurationKey(
175 'workerPoolMaxSize;',
177 "Use 'worker' section to define the worker pool maximum size instead"
179 Configuration
.warnDeprecatedConfigurationKey(
180 'workerPoolStrategy;',
182 "Use 'worker' section to define the worker pool strategy instead"
184 let workerConfiguration
: WorkerConfiguration
= {
185 processType
: Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'workerProcess')
186 ? Configuration
.getConfig().workerProcess
187 : WorkerProcessType
.WORKER_SET
,
188 startDelay
: Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'workerStartDelay')
189 ? Configuration
.getConfig().workerStartDelay
190 : WorkerConstants
.DEFAULT_WORKER_START_DELAY
,
191 elementsPerWorker
: Configuration
.objectHasOwnProperty(
192 Configuration
.getConfig(),
193 'chargingStationsPerWorker'
195 ? Configuration
.getConfig().chargingStationsPerWorker
196 : WorkerConstants
.DEFAULT_ELEMENTS_PER_WORKER
,
197 elementStartDelay
: Configuration
.objectHasOwnProperty(
198 Configuration
.getConfig(),
201 ? Configuration
.getConfig().elementStartDelay
202 : WorkerConstants
.DEFAULT_ELEMENT_START_DELAY
,
203 poolMinSize
: Configuration
.objectHasOwnProperty(
204 Configuration
.getConfig(),
207 ? Configuration
.getConfig().workerPoolMinSize
208 : WorkerConstants
.DEFAULT_POOL_MIN_SIZE
,
209 poolMaxSize
: Configuration
.objectHasOwnProperty(
210 Configuration
.getConfig(),
213 ? Configuration
.getConfig().workerPoolMaxSize
214 : WorkerConstants
.DEFAULT_POOL_MAX_SIZE
,
215 poolStrategy
: Configuration
.getConfig().workerPoolStrategy
,
217 if (Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'worker')) {
218 workerConfiguration
= { ...workerConfiguration
, ...Configuration
.getConfig().worker
};
220 return workerConfiguration
;
223 static getLogConsole(): boolean {
224 Configuration
.warnDeprecatedConfigurationKey('consoleLog', null, "Use 'logConsole' instead");
225 return Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'logConsole')
226 ? Configuration
.getConfig().logConsole
230 static getLogFormat(): string {
231 return Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'logFormat')
232 ? Configuration
.getConfig().logFormat
236 static getLogRotate(): boolean {
237 return Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'logRotate')
238 ? Configuration
.getConfig().logRotate
242 static getLogMaxFiles(): number {
243 return Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'logMaxFiles')
244 ? Configuration
.getConfig().logMaxFiles
248 static getLogLevel(): string {
249 return Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'logLevel')
250 ? Configuration
.getConfig().logLevel
.toLowerCase()
254 static getLogFile(): string {
255 return Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'logFile')
256 ? Configuration
.getConfig().logFile
260 static getLogErrorFile(): string {
261 Configuration
.warnDeprecatedConfigurationKey('errorFile', null, "Use 'logErrorFile' instead");
262 return Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'logErrorFile')
263 ? Configuration
.getConfig().logErrorFile
267 static getSupervisionUrls(): string | string[] {
268 Configuration
.warnDeprecatedConfigurationKey(
271 "Use 'supervisionUrls' instead"
273 !Configuration
.isUndefined(Configuration
.getConfig()['supervisionURLs']) &&
274 (Configuration
.getConfig().supervisionUrls
= Configuration
.getConfig()[
278 return Configuration
.getConfig().supervisionUrls
;
281 static getSupervisionUrlDistribution(): SupervisionUrlDistribution
{
282 Configuration
.warnDeprecatedConfigurationKey(
283 'distributeStationToTenantEqually',
285 "Use 'supervisionUrlDistribution' instead"
287 Configuration
.warnDeprecatedConfigurationKey(
288 'distributeStationsToTenantsEqually',
290 "Use 'supervisionUrlDistribution' instead"
292 return Configuration
.objectHasOwnProperty(
293 Configuration
.getConfig(),
294 'supervisionUrlDistribution'
296 ? Configuration
.getConfig().supervisionUrlDistribution
297 : SupervisionUrlDistribution
.ROUND_ROBIN
;
300 private static logPrefix(): string {
301 return new Date().toLocaleString() + ' Simulator configuration |';
304 private static warnDeprecatedConfigurationKey(
306 sectionName
?: string,
311 !Configuration
.isUndefined(Configuration
.getConfig()[sectionName
]) &&
312 !Configuration
.isUndefined(
313 (Configuration
.getConfig()[sectionName
] as Record
<string, unknown
>)[key
]
317 chalk
`{green ${Configuration.logPrefix()}} {red Deprecated configuration key '${key}' usage in section '${sectionName}'${
318 logMsgToAppend && '. ' + logMsgToAppend
321 } else if (!Configuration
.isUndefined(Configuration
.getConfig()[key
])) {
323 chalk
`{green ${Configuration.logPrefix()}} {red Deprecated configuration key '${key}' usage${
324 logMsgToAppend && '. ' + logMsgToAppend
330 // Read the config file
331 private static getConfig(): ConfigurationData
{
332 if (!Configuration
.configuration
) {
334 Configuration
.configuration
= JSON
.parse(
335 fs
.readFileSync(Configuration
.configurationFile
, 'utf8')
336 ) as ConfigurationData
;
338 Configuration
.handleFileException(
339 Configuration
.logPrefix(),
340 FileType
.Configuration
,
341 Configuration
.configurationFile
,
342 error
as NodeJS
.ErrnoException
345 if (!Configuration
.configurationFileWatcher
) {
346 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
349 return Configuration
.configuration
;
352 private static getConfigurationFileWatcher(): fs
.FSWatcher
{
354 return fs
.watch(Configuration
.configurationFile
, (event
, filename
): void => {
355 if (filename
&& event
=== 'change') {
356 // Nullify to force configuration file reading
357 Configuration
.configuration
= null;
358 if (!Configuration
.isUndefined(Configuration
.configurationChangeCallback
)) {
359 Configuration
.configurationChangeCallback().catch((error
) => {
360 throw typeof error
=== 'string' ? new Error(error
) : error
;
366 Configuration
.handleFileException(
367 Configuration
.logPrefix(),
368 FileType
.Configuration
,
369 Configuration
.configurationFile
,
370 error
as NodeJS
.ErrnoException
375 private static getDefaultPerformanceStorageUri(storageType
: StorageType
) {
376 const SQLiteFileName
= `${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`;
377 switch (storageType
) {
378 case StorageType
.JSON_FILE
:
379 return `file://${path.join(
380 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../'),
381 Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME
383 case StorageType
.SQLITE
:
384 return `file://${path.join(
385 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../'),
389 throw new Error(`Performance storage URI is mandatory with storage type '${storageType}'`);
393 private static isObject(item
): boolean {
394 return item
&& typeof item
=== 'object' && Array.isArray(item
) === false;
397 private static deepMerge(target
: object
, ...sources
: object
[]): object
{
398 if (!sources
.length
) {
401 const source
= sources
.shift();
403 if (Configuration
.isObject(target
) && Configuration
.isObject(source
)) {
404 for (const key
in source
) {
405 if (Configuration
.isObject(source
[key
])) {
407 Object.assign(target
, { [key
]: {} });
409 // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
410 Configuration
.deepMerge(target
[key
], source
[key
]);
412 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
413 Object.assign(target
, { [key
]: source
[key
] });
417 return Configuration
.deepMerge(target
, ...sources
);
420 private static objectHasOwnProperty(object
: unknown
, property
: string): boolean {
421 return Object.prototype
.hasOwnProperty
.call(object
, property
) as boolean;
424 private static isUndefined(obj
: unknown
): boolean {
425 return typeof obj
=== 'undefined';
428 private static handleFileException(
432 error
: NodeJS
.ErrnoException
,
433 params
: HandleErrorParams
<EmptyObject
> = { throwError
: true }
435 const prefix
= logPrefix
.length
!== 0 ? logPrefix
+ ' ' : '';
436 if (error
.code
=== 'ENOENT') {
438 chalk
.green(prefix
) + chalk
.red(fileType
+ ' file ' + filePath
+ ' not found: '),
441 } else if (error
.code
=== 'EEXIST') {
443 chalk
.green(prefix
) + chalk
.red(fileType
+ ' file ' + filePath
+ ' already exists: '),
446 } else if (error
.code
=== 'EACCES') {
448 chalk
.green(prefix
) + chalk
.red(fileType
+ ' file ' + filePath
+ ' access denied: '),
453 chalk
.green(prefix
) + chalk
.red(fileType
+ ' file ' + filePath
+ ' error: '),
457 if (params
?.throwError
) {