1 import { type FSWatcher
, readFileSync
, watch
} from
'node:fs';
2 import { dirname
, join
, resolve
} 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
'./Constants';
10 import { hasOwnProp
, isCFEnvironment
, isNotEmptyString
, isUndefined
} from
'./Utils';
13 type ConfigurationData
,
15 type LogConfiguration
,
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
= join(
27 dirname(fileURLToPath(import.meta
.url
)),
32 private static configurationFileWatcher
: FSWatcher
| undefined;
33 private static configuration
: ConfigurationData
| null = null;
34 private static configurationChangeCallback
: () => Promise
<void>;
36 private constructor() {
37 // This is intentional
40 public static setConfigurationChangeCallback(cb
: () => Promise
<void>): void {
41 Configuration
.configurationChangeCallback
= cb
;
44 public static getUIServer(): UIServerConfiguration
{
45 if (hasOwnProp(Configuration
.getConfig(), 'uiWebSocketServer')) {
47 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
48 "Deprecated configuration section 'uiWebSocketServer' usage. Use 'uiServer' instead"
52 let uiServerConfiguration
: UIServerConfiguration
= {
54 type: ApplicationProtocol
.WS
,
56 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
57 port
: Constants
.DEFAULT_UI_SERVER_PORT
,
60 if (hasOwnProp(Configuration
.getConfig(), 'uiServer')) {
61 uiServerConfiguration
= merge
<UIServerConfiguration
>(
62 uiServerConfiguration
,
63 Configuration
.getConfig()?.uiServer
66 if (isCFEnvironment() === true) {
67 delete uiServerConfiguration
.options
?.host
;
68 uiServerConfiguration
.options
.port
= parseInt(process
.env
.PORT
);
70 return uiServerConfiguration
;
73 public static getPerformanceStorage(): StorageConfiguration
{
74 Configuration
.warnDeprecatedConfigurationKey('URI', 'performanceStorage', "Use 'uri' instead");
75 let storageConfiguration
: StorageConfiguration
= {
77 type: StorageType
.JSON_FILE
,
78 uri
: this.getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
80 if (hasOwnProp(Configuration
.getConfig(), 'performanceStorage')) {
81 storageConfiguration
= {
82 ...storageConfiguration
,
83 ...Configuration
.getConfig()?.performanceStorage
,
84 ...(Configuration
.getConfig()?.performanceStorage
?.type === StorageType
.JSON_FILE
&&
85 Configuration
.getConfig()?.performanceStorage
?.uri
&& {
86 uri
: Configuration
.buildPerformanceUriFilePath(
87 new URL(Configuration
.getConfig()?.performanceStorage
?.uri
).pathname
92 return storageConfiguration
;
95 public static getAutoReconnectMaxRetries(): number | undefined {
96 Configuration
.warnDeprecatedConfigurationKey(
97 'autoReconnectTimeout',
99 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
101 Configuration
.warnDeprecatedConfigurationKey(
104 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
106 Configuration
.warnDeprecatedConfigurationKey(
107 'autoReconnectMaxRetries',
109 'Use it in charging station template instead'
112 if (hasOwnProp(Configuration
.getConfig(), 'autoReconnectMaxRetries')) {
113 return Configuration
.getConfig()?.autoReconnectMaxRetries
;
117 public static getStationTemplateUrls(): StationTemplateUrl
[] | undefined {
118 Configuration
.warnDeprecatedConfigurationKey(
119 'stationTemplateURLs',
121 "Use 'stationTemplateUrls' instead"
123 !isUndefined(Configuration
.getConfig()['stationTemplateURLs']) &&
124 (Configuration
.getConfig().stationTemplateUrls
= Configuration
.getConfig()[
125 'stationTemplateURLs'
126 ] as StationTemplateUrl
[]);
127 Configuration
.getConfig().stationTemplateUrls
.forEach(
128 (stationTemplateUrl
: StationTemplateUrl
) => {
129 if (!isUndefined(stationTemplateUrl
['numberOfStation'])) {
131 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
132 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`
139 return Configuration
.getConfig()?.stationTemplateUrls
;
142 public static getLog(): LogConfiguration
{
143 Configuration
.warnDeprecatedConfigurationKey(
146 "Use 'log' section to define the logging enablement instead"
148 Configuration
.warnDeprecatedConfigurationKey(
151 "Use 'log' section to define the log file instead"
153 Configuration
.warnDeprecatedConfigurationKey(
156 "Use 'log' section to define the log error file instead"
158 Configuration
.warnDeprecatedConfigurationKey(
161 "Use 'log' section to define the console logging enablement instead"
163 Configuration
.warnDeprecatedConfigurationKey(
164 'logStatisticsInterval',
166 "Use 'log' section to define the log statistics interval instead"
168 Configuration
.warnDeprecatedConfigurationKey(
171 "Use 'log' section to define the log level instead"
173 Configuration
.warnDeprecatedConfigurationKey(
176 "Use 'log' section to define the log format instead"
178 Configuration
.warnDeprecatedConfigurationKey(
181 "Use 'log' section to define the log rotation enablement instead"
183 Configuration
.warnDeprecatedConfigurationKey(
186 "Use 'log' section to define the log maximum files instead"
188 Configuration
.warnDeprecatedConfigurationKey(
191 "Use 'log' section to define the log maximum size instead"
193 const defaultLogConfiguration
: LogConfiguration
= {
195 file
: 'logs/combined.log',
196 errorFile
: 'logs/error.log',
197 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
202 const deprecatedLogConfiguration
: LogConfiguration
= {
203 ...(hasOwnProp(Configuration
.getConfig(), 'logEnabled') && {
204 enabled
: Configuration
.getConfig()?.logEnabled
,
206 ...(hasOwnProp(Configuration
.getConfig(), 'logFile') && {
207 file
: Configuration
.getConfig()?.logFile
,
209 ...(hasOwnProp(Configuration
.getConfig(), 'logErrorFile') && {
210 errorFile
: Configuration
.getConfig()?.logErrorFile
,
212 ...(hasOwnProp(Configuration
.getConfig(), 'logStatisticsInterval') && {
213 statisticsInterval
: Configuration
.getConfig()?.logStatisticsInterval
,
215 ...(hasOwnProp(Configuration
.getConfig(), 'logLevel') && {
216 level
: Configuration
.getConfig()?.logLevel
,
218 ...(hasOwnProp(Configuration
.getConfig(), 'logConsole') && {
219 console
: Configuration
.getConfig()?.logConsole
,
221 ...(hasOwnProp(Configuration
.getConfig(), 'logFormat') && {
222 format
: Configuration
.getConfig()?.logFormat
,
224 ...(hasOwnProp(Configuration
.getConfig(), 'logRotate') && {
225 rotate
: Configuration
.getConfig()?.logRotate
,
227 ...(hasOwnProp(Configuration
.getConfig(), 'logMaxFiles') && {
228 maxFiles
: Configuration
.getConfig()?.logMaxFiles
,
230 ...(hasOwnProp(Configuration
.getConfig(), 'logMaxSize') && {
231 maxSize
: Configuration
.getConfig()?.logMaxSize
,
234 const logConfiguration
: LogConfiguration
= {
235 ...defaultLogConfiguration
,
236 ...deprecatedLogConfiguration
,
237 ...(hasOwnProp(Configuration
.getConfig(), 'log') && Configuration
.getConfig()?.log
),
239 return logConfiguration
;
242 public static getWorker(): WorkerConfiguration
{
243 Configuration
.warnDeprecatedConfigurationKey(
246 "Use 'worker' section to define the type of worker process model instead"
248 Configuration
.warnDeprecatedConfigurationKey(
251 "Use 'worker' section to define the type of worker process model instead"
253 Configuration
.warnDeprecatedConfigurationKey(
256 "Use 'worker' section to define the worker start delay instead"
258 Configuration
.warnDeprecatedConfigurationKey(
259 'chargingStationsPerWorker',
261 "Use 'worker' section to define the number of element(s) per worker instead"
263 Configuration
.warnDeprecatedConfigurationKey(
266 "Use 'worker' section to define the worker's element start delay instead"
268 Configuration
.warnDeprecatedConfigurationKey(
271 "Use 'worker' section to define the worker pool minimum size instead"
273 Configuration
.warnDeprecatedConfigurationKey(
276 "Use 'worker' section to define the worker pool maximum size instead"
278 Configuration
.warnDeprecatedConfigurationKey(
279 'workerPoolMaxSize;',
281 "Use 'worker' section to define the worker pool maximum size instead"
283 Configuration
.warnDeprecatedConfigurationKey(
284 'workerPoolStrategy;',
286 "Use 'worker' section to define the worker pool strategy instead"
288 const defaultWorkerConfiguration
: WorkerConfiguration
= {
289 processType
: WorkerProcessType
.workerSet
,
290 startDelay
: WorkerConstants
.DEFAULT_WORKER_START_DELAY
,
291 elementsPerWorker
: WorkerConstants
.DEFAULT_ELEMENTS_PER_WORKER
,
292 elementStartDelay
: WorkerConstants
.DEFAULT_ELEMENT_START_DELAY
,
293 poolMinSize
: WorkerConstants
.DEFAULT_POOL_MIN_SIZE
,
294 poolMaxSize
: WorkerConstants
.DEFAULT_POOL_MAX_SIZE
,
295 poolStrategy
: WorkerChoiceStrategies
.ROUND_ROBIN
,
297 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
298 ...(hasOwnProp(Configuration
.getConfig(), 'workerProcess') && {
299 processType
: Configuration
.getConfig()?.workerProcess
,
301 ...(hasOwnProp(Configuration
.getConfig(), 'workerStartDelay') && {
302 startDelay
: Configuration
.getConfig()?.workerStartDelay
,
304 ...(hasOwnProp(Configuration
.getConfig(), 'chargingStationsPerWorker') && {
305 elementsPerWorker
: Configuration
.getConfig()?.chargingStationsPerWorker
,
307 ...(hasOwnProp(Configuration
.getConfig(), 'elementStartDelay') && {
308 elementStartDelay
: Configuration
.getConfig()?.elementStartDelay
,
310 ...(hasOwnProp(Configuration
.getConfig(), 'workerPoolMinSize') && {
311 poolMinSize
: Configuration
.getConfig()?.workerPoolMinSize
,
313 ...(hasOwnProp(Configuration
.getConfig(), 'workerPoolMaxSize') && {
314 poolMaxSize
: Configuration
.getConfig()?.workerPoolMaxSize
,
316 ...(hasOwnProp(Configuration
.getConfig(), 'workerPoolStrategy') && {
318 Configuration
.getConfig()?.workerPoolStrategy
?? WorkerChoiceStrategies
.ROUND_ROBIN
,
321 const workerConfiguration
: WorkerConfiguration
= {
322 ...defaultWorkerConfiguration
,
323 ...deprecatedWorkerConfiguration
,
324 ...(hasOwnProp(Configuration
.getConfig(), 'worker') && Configuration
.getConfig()?.worker
),
326 return workerConfiguration
;
329 public static workerPoolInUse(): boolean {
330 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.staticPool
].includes(
331 Configuration
.getWorker().processType
335 public static workerDynamicPoolInUse(): boolean {
336 return Configuration
.getWorker().processType
=== WorkerProcessType
.dynamicPool
;
339 public static getSupervisionUrls(): string | string[] | undefined {
340 Configuration
.warnDeprecatedConfigurationKey(
343 "Use 'supervisionUrls' instead"
345 !isUndefined(Configuration
.getConfig()['supervisionURLs']) &&
346 (Configuration
.getConfig().supervisionUrls
= Configuration
.getConfig()['supervisionURLs'] as
350 return Configuration
.getConfig()?.supervisionUrls
;
353 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
354 Configuration
.warnDeprecatedConfigurationKey(
355 'distributeStationToTenantEqually',
357 "Use 'supervisionUrlDistribution' instead"
359 Configuration
.warnDeprecatedConfigurationKey(
360 'distributeStationsToTenantsEqually',
362 "Use 'supervisionUrlDistribution' instead"
364 return hasOwnProp(Configuration
.getConfig(), 'supervisionUrlDistribution')
365 ? Configuration
.getConfig()?.supervisionUrlDistribution
366 : SupervisionUrlDistribution
.ROUND_ROBIN
;
369 private static logPrefix
= (): string => {
370 return `${new Date().toLocaleString()} Simulator configuration |`;
373 private static warnDeprecatedConfigurationKey(
375 sectionName
?: string,
380 !isUndefined(Configuration
.getConfig()[sectionName
]) &&
381 !isUndefined((Configuration
.getConfig()[sectionName
] as Record
<string, unknown
>)[key
])
384 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
385 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
386 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
390 } else if (!isUndefined(Configuration
.getConfig()[key
])) {
392 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
393 `Deprecated configuration key
'${key}' usage$
{
394 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
401 // Read the config file
402 private static getConfig(): ConfigurationData
| null {
403 if (!Configuration
.configuration
) {
405 Configuration
.configuration
= JSON
.parse(
406 readFileSync(Configuration
.configurationFile
, 'utf8')
407 ) as ConfigurationData
;
409 Configuration
.handleFileException(
410 Configuration
.configurationFile
,
411 FileType
.Configuration
,
412 error
as NodeJS
.ErrnoException
,
413 Configuration
.logPrefix()
416 if (!Configuration
.configurationFileWatcher
) {
417 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
420 return Configuration
.configuration
;
423 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
425 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
426 if (filename
?.trim().length
> 0 && event
=== 'change') {
427 // Nullify to force configuration file reading
428 Configuration
.configuration
= null;
429 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
430 Configuration
.configurationChangeCallback().catch((error
) => {
431 throw typeof error
=== 'string' ? new Error(error
) : error
;
437 Configuration
.handleFileException(
438 Configuration
.configurationFile
,
439 FileType
.Configuration
,
440 error
as NodeJS
.ErrnoException
,
441 Configuration
.logPrefix()
446 private static handleFileException(
449 error
: NodeJS
.ErrnoException
,
452 const prefix
= isNotEmptyString(logPrefix
) ? `${logPrefix} ` : '';
454 switch (error
.code
) {
456 logMsg
= `${fileType} file ${file} not found:`;
459 logMsg
= `${fileType} file ${file} already exists:`;
462 logMsg
= `${fileType} file ${file} access denied:`;
465 logMsg
= `${fileType} file ${file} permission denied:`;
468 logMsg
= `${fileType} file ${file} error:`;
470 console
.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
474 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
475 switch (storageType) {
476 case StorageType.JSON_FILE:
477 return Configuration.buildPerformanceUriFilePath(
478 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}
`
480 case StorageType.SQLITE:
481 return Configuration.buildPerformanceUriFilePath(
482 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}
.db
`
485 throw new Error(`Performance storage URI
is mandatory
with storage
type '${storageType}'`);
489 private static buildPerformanceUriFilePath(file: string) {
490 return `file
://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;