1 import { type FSWatcher
, readFileSync
, watch
} from
'node:fs';
2 import { dirname
, join
, resolve
} from
'node:path';
3 import { env
} from
'node:process';
4 import { fileURLToPath
} from
'node:url';
6 import chalk from
'chalk';
7 import merge from
'just-merge';
9 import { Constants
} from
'./Constants';
20 type ConfigurationData
,
23 type LogConfiguration
,
24 type StationTemplateUrl
,
25 type StorageConfiguration
,
27 SupervisionUrlDistribution
,
28 type UIServerConfiguration
,
29 type WorkerConfiguration
,
32 DEFAULT_ELEMENT_START_DELAY
,
33 DEFAULT_POOL_MAX_SIZE
,
34 DEFAULT_POOL_MIN_SIZE
,
35 DEFAULT_WORKER_START_DELAY
,
39 type ConfigurationSectionType
=
41 | StorageConfiguration
43 | UIServerConfiguration
;
45 // Avoid ESM race condition at class initialization
46 const configurationLogPrefix
= (): string => {
47 return logPrefix(' Simulator configuration |');
50 export class Configuration
{
51 public static configurationChangeCallback
: () => Promise
<void>;
53 private static configurationFile
= join(
54 dirname(fileURLToPath(import.meta
.url
)),
59 private static configurationFileReloading
= false;
60 private static configurationData
?: ConfigurationData
;
61 private static configurationFileWatcher
?: FSWatcher
;
62 private static configurationSectionCache
= new Map
<
64 ConfigurationSectionType
66 [ConfigurationSection
.log
, Configuration
.buildLogSection()],
67 [ConfigurationSection
.performanceStorage
, Configuration
.buildPerformanceStorageSection()],
68 [ConfigurationSection
.worker
, Configuration
.buildWorkerSection()],
69 [ConfigurationSection
.uiServer
, Configuration
.buildUIServerSection()],
72 private constructor() {
73 // This is intentional
76 public static getConfigurationSection
<T
extends ConfigurationSectionType
>(
77 sectionName
: ConfigurationSection
,
79 if (!Configuration
.isConfigurationSectionCached(sectionName
)) {
80 Configuration
.cacheConfigurationSection(sectionName
);
82 return Configuration
.configurationSectionCache
.get(sectionName
) as T
;
85 public static getStationTemplateUrls(): StationTemplateUrl
[] | undefined {
86 const checkDeprecatedConfigurationKeysOnce
= once(
87 Configuration
.checkDeprecatedConfigurationKeys
.bind(Configuration
),
90 checkDeprecatedConfigurationKeysOnce();
91 return Configuration
.getConfigurationData()?.stationTemplateUrls
;
94 public static getSupervisionUrls(): string | string[] | undefined {
97 Configuration
.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData
],
100 Configuration
.getConfigurationData()!.supervisionUrls
= Configuration
.getConfigurationData()![
101 'supervisionURLs' as keyof ConfigurationData
102 ] as string | string[];
104 return Configuration
.getConfigurationData()?.supervisionUrls
;
107 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
108 return hasOwnProp(Configuration
.getConfigurationData(), 'supervisionUrlDistribution')
109 ? Configuration
.getConfigurationData()?.supervisionUrlDistribution
110 : SupervisionUrlDistribution
.ROUND_ROBIN
;
113 public static workerPoolInUse(): boolean {
114 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.fixedPool
].includes(
115 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
120 public static workerDynamicPoolInUse(): boolean {
122 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
123 .processType
=== WorkerProcessType
.dynamicPool
127 private static isConfigurationSectionCached(sectionName
: ConfigurationSection
): boolean {
128 return Configuration
.configurationSectionCache
.has(sectionName
);
131 private static cacheConfigurationSection(sectionName
: ConfigurationSection
): void {
132 switch (sectionName
) {
133 case ConfigurationSection
.log
:
134 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildLogSection());
136 case ConfigurationSection
.performanceStorage
:
137 Configuration
.configurationSectionCache
.set(
139 Configuration
.buildPerformanceStorageSection(),
142 case ConfigurationSection
.worker
:
143 Configuration
.configurationSectionCache
.set(
145 Configuration
.buildWorkerSection(),
148 case ConfigurationSection
.uiServer
:
149 Configuration
.configurationSectionCache
.set(
151 Configuration
.buildUIServerSection(),
155 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
156 throw new Error(`Unknown configuration section '${sectionName}'`);
160 private static buildUIServerSection(): UIServerConfiguration
{
161 let uiServerConfiguration
: UIServerConfiguration
= {
163 type: ApplicationProtocol
.WS
,
165 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
166 port
: Constants
.DEFAULT_UI_SERVER_PORT
,
169 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
170 uiServerConfiguration
= merge
<UIServerConfiguration
>(
171 uiServerConfiguration
,
172 Configuration
.getConfigurationData()!.uiServer
!,
175 if (isCFEnvironment() === true) {
176 delete uiServerConfiguration
.options
?.host
;
177 uiServerConfiguration
.options
!.port
= parseInt(env
.PORT
!);
179 return uiServerConfiguration
;
182 private static buildPerformanceStorageSection(): StorageConfiguration
{
183 let storageConfiguration
: StorageConfiguration
= {
185 type: StorageType
.JSON_FILE
,
186 uri
: Configuration
.getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
188 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.performanceStorage
)) {
189 storageConfiguration
= {
190 ...storageConfiguration
,
191 ...Configuration
.getConfigurationData()?.performanceStorage
,
192 ...(Configuration
.getConfigurationData()?.performanceStorage
?.type ===
193 StorageType
.JSON_FILE
&&
194 Configuration
.getConfigurationData()?.performanceStorage
?.uri
&& {
195 uri
: Configuration
.buildPerformanceUriFilePath(
196 new URL(Configuration
.getConfigurationData()!.performanceStorage
!.uri
!).pathname
,
201 return storageConfiguration
;
204 private static buildLogSection(): LogConfiguration
{
205 const defaultLogConfiguration
: LogConfiguration
= {
207 file
: 'logs/combined.log',
208 errorFile
: 'logs/error.log',
209 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
214 const deprecatedLogConfiguration
: LogConfiguration
= {
215 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logEnabled') && {
216 enabled
: Configuration
.getConfigurationData()?.logEnabled
,
218 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
219 file
: Configuration
.getConfigurationData()?.logFile
,
221 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
222 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
,
224 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
225 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
,
227 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
228 level
: Configuration
.getConfigurationData()?.logLevel
,
230 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
231 console
: Configuration
.getConfigurationData()?.logConsole
,
233 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
234 format
: Configuration
.getConfigurationData()?.logFormat
,
236 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
237 rotate
: Configuration
.getConfigurationData()?.logRotate
,
239 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
240 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
,
242 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
243 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
,
246 const logConfiguration
: LogConfiguration
= {
247 ...defaultLogConfiguration
,
248 ...deprecatedLogConfiguration
,
249 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
250 Configuration
.getConfigurationData()?.log
),
252 return logConfiguration
;
255 private static buildWorkerSection(): WorkerConfiguration
{
256 const defaultWorkerConfiguration
: WorkerConfiguration
= {
257 processType
: WorkerProcessType
.workerSet
,
258 startDelay
: DEFAULT_WORKER_START_DELAY
,
259 elementsPerWorker
: 'auto',
260 elementStartDelay
: DEFAULT_ELEMENT_START_DELAY
,
261 poolMinSize
: DEFAULT_POOL_MIN_SIZE
,
262 poolMaxSize
: DEFAULT_POOL_MAX_SIZE
,
264 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
265 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
266 processType
: Configuration
.getConfigurationData()?.workerProcess
,
268 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
269 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
,
271 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
272 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
,
274 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementStartDelay') && {
275 elementStartDelay
: Configuration
.getConfigurationData()?.elementStartDelay
,
277 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
278 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
,
280 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
281 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
,
284 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
285 delete Configuration
.getConfigurationData()?.workerPoolStrategy
;
286 const workerConfiguration
: WorkerConfiguration
= {
287 ...defaultWorkerConfiguration
,
288 ...deprecatedWorkerConfiguration
,
289 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
290 Configuration
.getConfigurationData()?.worker
),
292 if (!Object.values(WorkerProcessType
).includes(workerConfiguration
.processType
!)) {
293 throw new SyntaxError(
294 `Invalid worker process type '${workerConfiguration.processType}' defined in configuration`,
297 return workerConfiguration
;
300 private static checkDeprecatedConfigurationKeys() {
301 // connection timeout
302 Configuration
.warnDeprecatedConfigurationKey(
303 'autoReconnectTimeout',
305 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
307 Configuration
.warnDeprecatedConfigurationKey(
310 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
312 // connection retries
313 Configuration
.warnDeprecatedConfigurationKey(
314 'autoReconnectMaxRetries',
316 'Use it in charging station template instead',
318 // station template url(s)
319 Configuration
.warnDeprecatedConfigurationKey(
320 'stationTemplateURLs',
322 "Use 'stationTemplateUrls' instead",
325 Configuration
.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData
],
327 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
328 Configuration
.getConfigurationData()![
329 'stationTemplateURLs' as keyof ConfigurationData
330 ] as StationTemplateUrl
[]);
331 Configuration
.getConfigurationData()?.stationTemplateUrls
.forEach(
332 (stationTemplateUrl
: StationTemplateUrl
) => {
333 if (!isUndefined(stationTemplateUrl
?.['numberOfStation' as keyof StationTemplateUrl
])) {
335 `${chalk.green(configurationLogPrefix())} ${chalk.red(
336 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`,
342 // supervision url(s)
343 Configuration
.warnDeprecatedConfigurationKey(
346 "Use 'supervisionUrls' instead",
348 // supervision urls distribution
349 Configuration
.warnDeprecatedConfigurationKey(
350 'distributeStationToTenantEqually',
352 "Use 'supervisionUrlDistribution' instead",
354 Configuration
.warnDeprecatedConfigurationKey(
355 'distributeStationsToTenantsEqually',
357 "Use 'supervisionUrlDistribution' instead",
360 Configuration
.warnDeprecatedConfigurationKey(
363 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
365 Configuration
.warnDeprecatedConfigurationKey(
368 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
370 Configuration
.warnDeprecatedConfigurationKey(
373 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`,
375 Configuration
.warnDeprecatedConfigurationKey(
376 'chargingStationsPerWorker',
378 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`,
380 Configuration
.warnDeprecatedConfigurationKey(
383 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`,
385 Configuration
.warnDeprecatedConfigurationKey(
388 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`,
390 Configuration
.warnDeprecatedConfigurationKey(
393 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
395 Configuration
.warnDeprecatedConfigurationKey(
398 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
400 Configuration
.warnDeprecatedConfigurationKey(
401 'workerPoolStrategy',
403 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`,
405 Configuration
.warnDeprecatedConfigurationKey(
407 ConfigurationSection
.worker
,
408 'Not publicly exposed to end users',
411 Configuration
.getConfigurationData()?.worker
?.processType
===
412 ('staticPool' as WorkerProcessType
)
415 `${chalk.green(configurationLogPrefix())} ${chalk.red(
416 `Deprecated configuration
'staticPool' value usage
in worker section
'processType' field
. Use
'${WorkerProcessType.fixedPool}' value instead
`,
421 Configuration
.warnDeprecatedConfigurationKey(
424 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
426 Configuration
.warnDeprecatedConfigurationKey(
429 `Use '${ConfigurationSection.log}' section to define the log file instead`,
431 Configuration
.warnDeprecatedConfigurationKey(
434 `Use '${ConfigurationSection.log}' section to define the log error file instead`,
436 Configuration
.warnDeprecatedConfigurationKey(
439 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
441 Configuration
.warnDeprecatedConfigurationKey(
442 'logStatisticsInterval',
444 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
446 Configuration
.warnDeprecatedConfigurationKey(
449 `Use '${ConfigurationSection.log}' section to define the log level instead`,
451 Configuration
.warnDeprecatedConfigurationKey(
454 `Use '${ConfigurationSection.log}' section to define the log format instead`,
456 Configuration
.warnDeprecatedConfigurationKey(
459 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
461 Configuration
.warnDeprecatedConfigurationKey(
464 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
466 Configuration
.warnDeprecatedConfigurationKey(
469 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
471 // performanceStorage section
472 Configuration
.warnDeprecatedConfigurationKey(
474 ConfigurationSection
.performanceStorage
,
478 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
480 `${chalk.green(configurationLogPrefix())} ${chalk.red(
481 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`,
487 private static warnDeprecatedConfigurationKey(
489 sectionName
?: string,
495 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
],
499 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
] as Record
<
507 `${chalk.green(configurationLogPrefix())} ${chalk.red(
508 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
509 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
514 !isUndefined(Configuration
.getConfigurationData()?.[key
as keyof ConfigurationData
])
517 `${chalk.green(configurationLogPrefix())} ${chalk.red(
518 `Deprecated configuration key
'${key}' usage$
{
519 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
526 private static getConfigurationData(): ConfigurationData
| undefined {
527 if (!Configuration
.configurationData
) {
529 Configuration
.configurationData
= JSON
.parse(
530 readFileSync(Configuration
.configurationFile
, 'utf8'),
531 ) as ConfigurationData
;
532 if (!Configuration
.configurationFileWatcher
) {
533 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
536 Configuration
.handleFileException(
537 Configuration
.configurationFile
,
538 FileType
.Configuration
,
539 error
as NodeJS
.ErrnoException
,
540 configurationLogPrefix(),
544 return Configuration
.configurationData
;
547 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
549 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
551 !Configuration
.configurationFileReloading
&&
552 filename
!.trim()!.length
> 0 &&
555 Configuration
.configurationFileReloading
= true;
556 const consoleWarnOnce
= once(console
.warn
, this);
558 `${chalk.green(configurationLogPrefix())} ${chalk.yellow(
559 `${FileType.Configuration} ${this.configurationFile} file have changed
, reload
`,
562 delete Configuration
.configurationData
;
563 Configuration
.configurationSectionCache
.clear();
564 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
565 Configuration
.configurationChangeCallback()
567 throw typeof error
=== 'string' ? new Error(error
) : error
;
570 Configuration
.configurationFileReloading
= false;
573 Configuration
.configurationFileReloading
= false;
578 Configuration
.handleFileException(
579 Configuration
.configurationFile
,
580 FileType
.Configuration
,
581 error
as NodeJS
.ErrnoException
,
582 configurationLogPrefix(),
587 private static handleFileException(
590 error
: NodeJS
.ErrnoException
,
593 const prefix
= isNotEmptyString(logPfx
) ? `${logPfx} ` : '';
595 switch (error
.code
) {
597 logMsg
= `${fileType} file ${file} not found: `;
600 logMsg
= `${fileType} file ${file} already exists: `;
603 logMsg
= `${fileType} file ${file} access denied: `;
606 logMsg
= `${fileType} file ${file} permission denied: `;
609 logMsg
= `${fileType} file ${file} error: `;
611 console
.error(`${chalk.green(prefix)}${chalk.red(logMsg)}`, error
);
615 private static getDefaultPerformanceStorageUri(storageType
: StorageType
) {
616 switch (storageType
) {
617 case StorageType
.JSON_FILE
:
618 return Configuration
.buildPerformanceUriFilePath(
619 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}`,
621 case StorageType
.SQLITE
:
622 return Configuration
.buildPerformanceUriFilePath(
623 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`,
626 throw new Error(`Unsupported storage type '${storageType}'`);
630 private static buildPerformanceUriFilePath(file
: string) {
631 return `file://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;