1 import fs from
'node:fs';
2 import path from
'node:path';
3 import { fileURLToPath
} from
'node:url';
5 import chalk from
'chalk';
6 import merge from
'just-merge';
7 import { WorkerChoiceStrategies
} from
'poolifier';
9 import { Constants
} from
'./internal';
12 type ConfigurationData
,
15 type HandleErrorParams
,
16 type StationTemplateUrl
,
17 type StorageConfiguration
,
19 SupervisionUrlDistribution
,
20 type UIServerConfiguration
,
21 type WorkerConfiguration
,
23 import { WorkerConstants
, WorkerProcessType
} from
'../worker';
25 export class Configuration
{
26 private static configurationFile
= path
.join(
27 path
.resolve(path
.dirname(fileURLToPath(import.meta
.url
)), '../'),
32 private static configurationFileWatcher
: fs
.FSWatcher
| undefined;
33 private static configuration
: ConfigurationData
| null = null;
34 private static configurationChangeCallback
: () => Promise
<void>;
36 private constructor() {
37 // This is intentional
40 static setConfigurationChangeCallback(cb
: () => Promise
<void>): void {
41 Configuration
.configurationChangeCallback
= cb
;
44 static getLogStatisticsInterval(): number | undefined {
45 Configuration
.warnDeprecatedConfigurationKey(
46 'statisticsDisplayInterval',
48 "Use 'logStatisticsInterval' instead"
51 return Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'logStatisticsInterval')
52 ? Configuration
.getConfig()?.logStatisticsInterval
53 : Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
;
56 static getUIServer(): UIServerConfiguration
{
57 if (Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'uiWebSocketServer')) {
59 chalk
`{green ${Configuration.logPrefix()}} {red Deprecated configuration section 'uiWebSocketServer' usage. Use 'uiServer' instead}`
62 let uiServerConfiguration
: UIServerConfiguration
= {
64 type: ApplicationProtocol
.WS
,
66 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
67 port
: Constants
.DEFAULT_UI_SERVER_PORT
,
70 if (Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'uiServer')) {
71 uiServerConfiguration
= merge
<UIServerConfiguration
>(
72 uiServerConfiguration
,
73 Configuration
.getConfig()?.uiServer
76 if (Configuration
.isCFEnvironment() === true) {
77 delete uiServerConfiguration
.options
?.host
;
78 uiServerConfiguration
.options
.port
= parseInt(process
.env
.PORT
);
80 return uiServerConfiguration
;
83 static getPerformanceStorage(): StorageConfiguration
{
84 Configuration
.warnDeprecatedConfigurationKey('URI', 'performanceStorage', "Use 'uri' instead");
85 let storageConfiguration
: StorageConfiguration
= {
87 type: StorageType
.JSON_FILE
,
88 uri
: this.getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
90 if (Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'performanceStorage')) {
91 storageConfiguration
= {
92 ...storageConfiguration
,
93 ...Configuration
.getConfig()?.performanceStorage
,
96 return storageConfiguration
;
99 static getAutoReconnectMaxRetries(): number | undefined {
100 Configuration
.warnDeprecatedConfigurationKey(
101 'autoReconnectTimeout',
103 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
105 Configuration
.warnDeprecatedConfigurationKey(
108 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
110 Configuration
.warnDeprecatedConfigurationKey(
111 'autoReconnectMaxRetries',
113 'Use it in charging station template instead'
116 if (Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'autoReconnectMaxRetries')) {
117 return Configuration
.getConfig()?.autoReconnectMaxRetries
;
121 static getStationTemplateUrls(): StationTemplateUrl
[] | undefined {
122 Configuration
.warnDeprecatedConfigurationKey(
123 'stationTemplateURLs',
125 "Use 'stationTemplateUrls' instead"
127 !Configuration
.isUndefined(Configuration
.getConfig()['stationTemplateURLs']) &&
128 (Configuration
.getConfig().stationTemplateUrls
= Configuration
.getConfig()[
129 'stationTemplateURLs'
130 ] as unknown
as StationTemplateUrl
[]);
131 Configuration
.getConfig().stationTemplateUrls
.forEach((stationUrl
: StationTemplateUrl
) => {
132 if (!Configuration
.isUndefined(stationUrl
['numberOfStation'])) {
134 chalk
`{green ${Configuration.logPrefix()}} {red Deprecated configuration key 'numberOfStation' usage for template file '${
136 }' in 'stationTemplateUrls'. Use 'numberOfStations' instead}`
141 return Configuration
.getConfig()?.stationTemplateUrls
;
144 static getWorker(): WorkerConfiguration
{
145 Configuration
.warnDeprecatedConfigurationKey(
148 "Use 'worker' section to define the type of worker process model instead"
150 Configuration
.warnDeprecatedConfigurationKey(
153 "Use 'worker' section to define the type of worker process model instead"
155 Configuration
.warnDeprecatedConfigurationKey(
158 "Use 'worker' section to define the worker start delay instead"
160 Configuration
.warnDeprecatedConfigurationKey(
161 'chargingStationsPerWorker',
163 "Use 'worker' section to define the number of element(s) per worker instead"
165 Configuration
.warnDeprecatedConfigurationKey(
168 "Use 'worker' section to define the worker's element start delay instead"
170 Configuration
.warnDeprecatedConfigurationKey(
173 "Use 'worker' section to define the worker pool minimum size instead"
175 Configuration
.warnDeprecatedConfigurationKey(
178 "Use 'worker' section to define the worker pool maximum size instead"
180 Configuration
.warnDeprecatedConfigurationKey(
181 'workerPoolMaxSize;',
183 "Use 'worker' section to define the worker pool maximum size instead"
185 Configuration
.warnDeprecatedConfigurationKey(
186 'workerPoolStrategy;',
188 "Use 'worker' section to define the worker pool strategy instead"
190 let workerConfiguration
: WorkerConfiguration
= {
191 processType
: Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'workerProcess')
192 ? Configuration
.getConfig()?.workerProcess
193 : WorkerProcessType
.WORKER_SET
,
194 startDelay
: Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'workerStartDelay')
195 ? Configuration
.getConfig()?.workerStartDelay
196 : WorkerConstants
.DEFAULT_WORKER_START_DELAY
,
197 elementsPerWorker
: Configuration
.objectHasOwnProperty(
198 Configuration
.getConfig(),
199 'chargingStationsPerWorker'
201 ? Configuration
.getConfig()?.chargingStationsPerWorker
202 : WorkerConstants
.DEFAULT_ELEMENTS_PER_WORKER
,
203 elementStartDelay
: Configuration
.objectHasOwnProperty(
204 Configuration
.getConfig(),
207 ? Configuration
.getConfig()?.elementStartDelay
208 : WorkerConstants
.DEFAULT_ELEMENT_START_DELAY
,
209 poolMinSize
: Configuration
.objectHasOwnProperty(
210 Configuration
.getConfig(),
213 ? Configuration
.getConfig()?.workerPoolMinSize
214 : WorkerConstants
.DEFAULT_POOL_MIN_SIZE
,
215 poolMaxSize
: Configuration
.objectHasOwnProperty(
216 Configuration
.getConfig(),
219 ? Configuration
.getConfig()?.workerPoolMaxSize
220 : WorkerConstants
.DEFAULT_POOL_MAX_SIZE
,
222 Configuration
.getConfig()?.workerPoolStrategy
?? WorkerChoiceStrategies
.ROUND_ROBIN
,
224 if (Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'worker')) {
225 workerConfiguration
= { ...workerConfiguration
, ...Configuration
.getConfig()?.worker
};
227 return workerConfiguration
;
230 static getLogConsole(): boolean | undefined {
231 Configuration
.warnDeprecatedConfigurationKey(
234 "Use 'logConsole' instead"
236 return Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'logConsole')
237 ? Configuration
.getConfig()?.logConsole
241 static getLogFormat(): string | undefined {
242 return Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'logFormat')
243 ? Configuration
.getConfig()?.logFormat
247 static getLogRotate(): boolean | undefined {
248 return Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'logRotate')
249 ? Configuration
.getConfig()?.logRotate
253 static getLogMaxFiles(): number | string | false | undefined {
255 Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'logMaxFiles') &&
256 Configuration
.getConfig()?.logMaxFiles
260 static getLogMaxSize(): number | string | false | undefined {
262 Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'logMaxFiles') &&
263 Configuration
.getConfig()?.logMaxSize
267 static getLogLevel(): string | undefined {
268 return Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'logLevel')
269 ? Configuration
.getConfig()?.logLevel
?.toLowerCase()
273 static getLogFile(): string | undefined {
274 return Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'logFile')
275 ? Configuration
.getConfig()?.logFile
279 static getLogErrorFile(): string | undefined {
280 Configuration
.warnDeprecatedConfigurationKey(
283 "Use 'logErrorFile' instead"
285 return Configuration
.objectHasOwnProperty(Configuration
.getConfig(), 'logErrorFile')
286 ? Configuration
.getConfig()?.logErrorFile
290 static getSupervisionUrls(): string | string[] | undefined {
291 Configuration
.warnDeprecatedConfigurationKey(
294 "Use 'supervisionUrls' instead"
296 !Configuration
.isUndefined(Configuration
.getConfig()['supervisionURLs']) &&
297 (Configuration
.getConfig().supervisionUrls
= Configuration
.getConfig()['supervisionURLs'] as
301 return Configuration
.getConfig()?.supervisionUrls
;
304 static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
305 Configuration
.warnDeprecatedConfigurationKey(
306 'distributeStationToTenantEqually',
308 "Use 'supervisionUrlDistribution' instead"
310 Configuration
.warnDeprecatedConfigurationKey(
311 'distributeStationsToTenantsEqually',
313 "Use 'supervisionUrlDistribution' instead"
315 return Configuration
.objectHasOwnProperty(
316 Configuration
.getConfig(),
317 'supervisionUrlDistribution'
319 ? Configuration
.getConfig()?.supervisionUrlDistribution
320 : SupervisionUrlDistribution
.ROUND_ROBIN
;
323 private static logPrefix
= (): string => {
324 return `${new Date().toLocaleString()} Simulator configuration |`;
327 private static warnDeprecatedConfigurationKey(
329 sectionName
?: string,
334 !Configuration
.isUndefined(Configuration
.getConfig()[sectionName
]) &&
335 !Configuration
.isUndefined(
336 (Configuration
.getConfig()[sectionName
] as Record
<string, unknown
>)[key
]
340 chalk
`{green ${Configuration.logPrefix()}} {red Deprecated configuration key '${key}' usage in section '${sectionName}'${
341 logMsgToAppend.trim().length > 0 && `. ${logMsgToAppend}
`
344 } else if (!Configuration
.isUndefined(Configuration
.getConfig()[key
])) {
346 chalk
`{green ${Configuration.logPrefix()}} {red Deprecated configuration key '${key}' usage${
347 logMsgToAppend.trim().length > 0 && `. ${logMsgToAppend}
`
353 // Read the config file
354 private static getConfig(): ConfigurationData
| null {
355 if (!Configuration
.configuration
) {
357 Configuration
.configuration
= JSON
.parse(
358 fs
.readFileSync(Configuration
.configurationFile
, 'utf8')
359 ) as ConfigurationData
;
361 Configuration
.handleFileException(
362 Configuration
.configurationFile
,
363 FileType
.Configuration
,
364 error
as NodeJS
.ErrnoException
,
365 Configuration
.logPrefix()
368 if (!Configuration
.configurationFileWatcher
) {
369 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
372 return Configuration
.configuration
;
375 private static getConfigurationFileWatcher(): fs
.FSWatcher
| undefined {
377 return fs
.watch(Configuration
.configurationFile
, (event
, filename
): void => {
378 if (filename
?.trim().length
> 0 && event
=== 'change') {
379 // Nullify to force configuration file reading
380 Configuration
.configuration
= null;
381 if (!Configuration
.isUndefined(Configuration
.configurationChangeCallback
)) {
382 Configuration
.configurationChangeCallback().catch((error
) => {
383 throw typeof error
=== 'string' ? new Error(error
) : error
;
389 Configuration
.handleFileException(
390 Configuration
.configurationFile
,
391 FileType
.Configuration
,
392 error
as NodeJS
.ErrnoException
,
393 Configuration
.logPrefix()
398 private static isCFEnvironment(): boolean {
399 return process
.env
.VCAP_APPLICATION
!== undefined;
402 private static getDefaultPerformanceStorageUri(storageType
: StorageType
) {
403 switch (storageType
) {
404 case StorageType
.JSON_FILE
:
405 return `file://${path.join(
406 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../'),
407 Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME
409 case StorageType
.SQLITE
:
410 return `file://${path.join(
411 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../'),
412 `${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}
.db
`
415 throw new Error(`Performance storage URI is mandatory with storage type '${storageType}'`);
419 private static objectHasOwnProperty(object
: unknown
, property
: string): boolean {
420 return Object.prototype
.hasOwnProperty
.call(object
, property
) as boolean;
423 private static isUndefined(obj
: unknown
): boolean {
424 return obj
=== undefined;
427 private static handleFileException(
430 error
: NodeJS
.ErrnoException
,
432 params
: HandleErrorParams
<EmptyObject
> = { throwError
: true }
434 const prefix
= logPrefix
?.trim().length
> 0 ? `${logPrefix} ` : '';
436 switch (error
.code
) {
438 logMsg
= `${fileType} file ${filePath} not found: `;
441 logMsg
= `${fileType} file ${filePath} already exists: `;
444 logMsg
= `${fileType} file ${filePath} access denied: `;
447 logMsg
= `${fileType} file ${filePath} error: `;
449 console
.error(`${chalk.green(prefix)}${chalk.red(logMsg)}`, error
);
450 if (params
?.throwError
) {