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';
8 import { Constants
} from
'./Constants';
9 import { hasOwnProp
, isCFEnvironment
, isNotEmptyString
, isUndefined
} from
'./Utils';
12 type ConfigurationData
,
15 type LogConfiguration
,
16 type StationTemplateUrl
,
17 type StorageConfiguration
,
19 SupervisionUrlDistribution
,
20 type UIServerConfiguration
,
21 type WorkerConfiguration
,
24 DEFAULT_ELEMENT_START_DELAY
,
25 DEFAULT_POOL_MAX_SIZE
,
26 DEFAULT_POOL_MIN_SIZE
,
27 DEFAULT_WORKER_START_DELAY
,
31 type ConfigurationSectionType
=
33 | StorageConfiguration
35 | UIServerConfiguration
;
37 export class Configuration
{
38 private static configurationFile
= join(
39 dirname(fileURLToPath(import.meta
.url
)),
44 private static configurationFileWatcher
?: FSWatcher
;
45 private static configurationData
?: ConfigurationData
;
46 private static configurationSectionCache
= new Map
<
48 ConfigurationSectionType
50 [ConfigurationSection
.log
, Configuration
.buildLogSection()],
51 [ConfigurationSection
.performanceStorage
, Configuration
.buildPerformanceStorageSection()],
52 [ConfigurationSection
.worker
, Configuration
.buildWorkerSection()],
53 [ConfigurationSection
.uiServer
, Configuration
.buildUIServerSection()],
56 private static configurationChangeCallback
?: () => Promise
<void>;
58 private constructor() {
59 // This is intentional
62 public static setConfigurationChangeCallback(cb
: () => Promise
<void>): void {
63 Configuration
.configurationChangeCallback
= cb
;
66 public static getConfigurationSection
<T
extends ConfigurationSectionType
>(
67 sectionName
: ConfigurationSection
,
69 if (!Configuration
.isConfigurationSectionCached(sectionName
)) {
70 Configuration
.cacheConfigurationSection(sectionName
);
72 return Configuration
.configurationSectionCache
.get(sectionName
) as T
;
75 public static getAutoReconnectMaxRetries(): number | undefined {
76 Configuration
.warnDeprecatedConfigurationKey(
77 'autoReconnectTimeout',
79 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
81 Configuration
.warnDeprecatedConfigurationKey(
84 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
86 Configuration
.warnDeprecatedConfigurationKey(
87 'autoReconnectMaxRetries',
89 'Use it in charging station template instead',
91 if (hasOwnProp(Configuration
.getConfigurationData(), 'autoReconnectMaxRetries')) {
92 return Configuration
.getConfigurationData()?.autoReconnectMaxRetries
;
96 public static getStationTemplateUrls(): StationTemplateUrl
[] | undefined {
97 Configuration
.warnDeprecatedConfigurationKey(
98 'stationTemplateURLs',
100 "Use 'stationTemplateUrls' instead",
102 // eslint-disable-next-line @typescript-eslint/dot-notation
104 Configuration
.getConfigurationData()!['stationTemplateURLs' as keyof ConfigurationData
],
106 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
107 Configuration
.getConfigurationData()![
108 // eslint-disable-next-line @typescript-eslint/dot-notation
109 'stationTemplateURLs' as keyof ConfigurationData
110 ] as StationTemplateUrl
[]);
111 Configuration
.getConfigurationData()!.stationTemplateUrls
.forEach(
112 (stationTemplateUrl
: StationTemplateUrl
) => {
113 // eslint-disable-next-line @typescript-eslint/dot-notation
114 if (!isUndefined(stationTemplateUrl
['numberOfStation' as keyof StationTemplateUrl
])) {
116 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
117 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`,
123 return Configuration
.getConfigurationData()?.stationTemplateUrls
;
126 public static getSupervisionUrls(): string | string[] | undefined {
127 Configuration
.warnDeprecatedConfigurationKey(
130 "Use 'supervisionUrls' instead",
132 // eslint-disable-next-line @typescript-eslint/dot-notation
135 Configuration
.getConfigurationData()!['supervisionURLs' as keyof ConfigurationData
],
138 Configuration
.getConfigurationData()!.supervisionUrls
= Configuration
.getConfigurationData()![
139 // eslint-disable-next-line @typescript-eslint/dot-notation
140 'supervisionURLs' as keyof ConfigurationData
141 ] as string | string[];
143 return Configuration
.getConfigurationData()?.supervisionUrls
;
146 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
147 Configuration
.warnDeprecatedConfigurationKey(
148 'distributeStationToTenantEqually',
150 "Use 'supervisionUrlDistribution' instead",
152 Configuration
.warnDeprecatedConfigurationKey(
153 'distributeStationsToTenantsEqually',
155 "Use 'supervisionUrlDistribution' instead",
157 return hasOwnProp(Configuration
.getConfigurationData(), 'supervisionUrlDistribution')
158 ? Configuration
.getConfigurationData()?.supervisionUrlDistribution
159 : SupervisionUrlDistribution
.ROUND_ROBIN
;
162 public static workerPoolInUse(): boolean {
163 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.staticPool
].includes(
164 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
169 public static workerDynamicPoolInUse(): boolean {
171 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
172 .processType
=== WorkerProcessType
.dynamicPool
176 private static isConfigurationSectionCached(sectionName
: ConfigurationSection
): boolean {
177 return Configuration
.configurationSectionCache
.has(sectionName
);
180 private static cacheConfigurationSection(sectionName
: ConfigurationSection
): void {
181 switch (sectionName
) {
182 case ConfigurationSection
.log
:
183 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildLogSection());
185 case ConfigurationSection
.performanceStorage
:
186 Configuration
.configurationSectionCache
.set(
188 Configuration
.buildPerformanceStorageSection(),
191 case ConfigurationSection
.worker
:
192 Configuration
.configurationSectionCache
.set(
194 Configuration
.buildWorkerSection(),
197 case ConfigurationSection
.uiServer
:
198 Configuration
.configurationSectionCache
.set(
200 Configuration
.buildUIServerSection(),
204 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
205 throw new Error(`Unknown configuration section '${sectionName}'`);
209 private static buildUIServerSection(): UIServerConfiguration
{
210 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
212 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
213 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`,
217 let uiServerConfiguration
: UIServerConfiguration
= {
219 type: ApplicationProtocol
.WS
,
221 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
222 port
: Constants
.DEFAULT_UI_SERVER_PORT
,
225 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
226 uiServerConfiguration
= merge
<UIServerConfiguration
>(
227 uiServerConfiguration
,
228 Configuration
.getConfigurationData()!.uiServer
!,
231 if (isCFEnvironment() === true) {
232 delete uiServerConfiguration
.options
?.host
;
233 uiServerConfiguration
.options
!.port
= parseInt(process
.env
.PORT
!);
235 return uiServerConfiguration
;
238 private static buildPerformanceStorageSection(): StorageConfiguration
{
239 Configuration
.warnDeprecatedConfigurationKey(
241 ConfigurationSection
.performanceStorage
,
244 let storageConfiguration
: StorageConfiguration
= {
246 type: StorageType
.JSON_FILE
,
247 uri
: Configuration
.getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
249 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.performanceStorage
)) {
250 storageConfiguration
= {
251 ...storageConfiguration
,
252 ...Configuration
.getConfigurationData()?.performanceStorage
,
253 ...(Configuration
.getConfigurationData()?.performanceStorage
?.type ===
254 StorageType
.JSON_FILE
&&
255 Configuration
.getConfigurationData()?.performanceStorage
?.uri
&& {
256 uri
: Configuration
.buildPerformanceUriFilePath(
257 new URL(Configuration
.getConfigurationData()!.performanceStorage
!.uri
!).pathname
,
262 return storageConfiguration
;
265 private static buildLogSection(): LogConfiguration
{
266 Configuration
.warnDeprecatedConfigurationKey(
269 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
271 Configuration
.warnDeprecatedConfigurationKey(
274 `Use '${ConfigurationSection.log}' section to define the log file instead`,
276 Configuration
.warnDeprecatedConfigurationKey(
279 `Use '${ConfigurationSection.log}' section to define the log error file instead`,
281 Configuration
.warnDeprecatedConfigurationKey(
284 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
286 Configuration
.warnDeprecatedConfigurationKey(
287 'logStatisticsInterval',
289 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
291 Configuration
.warnDeprecatedConfigurationKey(
294 `Use '${ConfigurationSection.log}' section to define the log level instead`,
296 Configuration
.warnDeprecatedConfigurationKey(
299 `Use '${ConfigurationSection.log}' section to define the log format instead`,
301 Configuration
.warnDeprecatedConfigurationKey(
304 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
306 Configuration
.warnDeprecatedConfigurationKey(
309 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
311 Configuration
.warnDeprecatedConfigurationKey(
314 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
316 const defaultLogConfiguration
: LogConfiguration
= {
318 file
: 'logs/combined.log',
319 errorFile
: 'logs/error.log',
320 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
325 const deprecatedLogConfiguration
: LogConfiguration
= {
326 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logEnabled') && {
327 enabled
: Configuration
.getConfigurationData()?.logEnabled
,
329 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
330 file
: Configuration
.getConfigurationData()?.logFile
,
332 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
333 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
,
335 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
336 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
,
338 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
339 level
: Configuration
.getConfigurationData()?.logLevel
,
341 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
342 console
: Configuration
.getConfigurationData()?.logConsole
,
344 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
345 format
: Configuration
.getConfigurationData()?.logFormat
,
347 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
348 rotate
: Configuration
.getConfigurationData()?.logRotate
,
350 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
351 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
,
353 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
354 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
,
357 const logConfiguration
: LogConfiguration
= {
358 ...defaultLogConfiguration
,
359 ...deprecatedLogConfiguration
,
360 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
361 Configuration
.getConfigurationData()?.log
),
363 return logConfiguration
;
366 private static buildWorkerSection(): WorkerConfiguration
{
367 Configuration
.warnDeprecatedConfigurationKey(
370 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
372 Configuration
.warnDeprecatedConfigurationKey(
375 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
377 Configuration
.warnDeprecatedConfigurationKey(
380 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`,
382 Configuration
.warnDeprecatedConfigurationKey(
383 'chargingStationsPerWorker',
385 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`,
387 Configuration
.warnDeprecatedConfigurationKey(
390 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`,
392 Configuration
.warnDeprecatedConfigurationKey(
395 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`,
397 Configuration
.warnDeprecatedConfigurationKey(
400 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
402 Configuration
.warnDeprecatedConfigurationKey(
403 'workerPoolMaxSize;',
405 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
407 Configuration
.warnDeprecatedConfigurationKey(
408 'workerPoolStrategy;',
410 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`,
412 const defaultWorkerConfiguration
: WorkerConfiguration
= {
413 processType
: WorkerProcessType
.workerSet
,
414 startDelay
: DEFAULT_WORKER_START_DELAY
,
415 elementsPerWorker
: 'auto',
416 elementStartDelay
: DEFAULT_ELEMENT_START_DELAY
,
417 poolMinSize
: DEFAULT_POOL_MIN_SIZE
,
418 poolMaxSize
: DEFAULT_POOL_MAX_SIZE
,
420 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
421 delete Configuration
.getConfigurationData()?.workerPoolStrategy
;
422 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
423 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
424 processType
: Configuration
.getConfigurationData()?.workerProcess
,
426 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
427 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
,
429 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
430 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
,
432 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementStartDelay') && {
433 elementStartDelay
: Configuration
.getConfigurationData()?.elementStartDelay
,
435 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
436 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
,
438 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
439 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
,
442 Configuration
.warnDeprecatedConfigurationKey(
444 ConfigurationSection
.worker
,
445 'Not publicly exposed to end users',
447 const workerConfiguration
: WorkerConfiguration
= {
448 ...defaultWorkerConfiguration
,
449 ...deprecatedWorkerConfiguration
,
450 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
451 Configuration
.getConfigurationData()?.worker
),
453 if (!Object.values(WorkerProcessType
).includes(workerConfiguration
.processType
!)) {
454 throw new SyntaxError(
455 `Invalid worker process type '${workerConfiguration.processType}' defined in configuration`,
458 return workerConfiguration
;
461 private static logPrefix
= (): string => {
462 return `${new Date().toLocaleString()} Simulator configuration |`;
465 private static warnDeprecatedConfigurationKey(
467 sectionName
?: string,
472 !isUndefined(Configuration
.getConfigurationData()![sectionName
as keyof ConfigurationData
]) &&
475 Configuration
.getConfigurationData()![sectionName
as keyof ConfigurationData
] as Record
<
483 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
484 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
485 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
490 !isUndefined(Configuration
.getConfigurationData()![key
as keyof ConfigurationData
])
493 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
494 `Deprecated configuration key
'${key}' usage$
{
495 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
502 private static getConfigurationData(): ConfigurationData
| undefined {
503 if (!Configuration
.configurationData
) {
505 Configuration
.configurationData
= JSON
.parse(
506 readFileSync(Configuration
.configurationFile
, 'utf8'),
507 ) as ConfigurationData
;
508 if (!Configuration
.configurationFileWatcher
) {
509 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
512 Configuration
.handleFileException(
513 Configuration
.configurationFile
,
514 FileType
.Configuration
,
515 error
as NodeJS
.ErrnoException
,
516 Configuration
.logPrefix(),
520 return Configuration
.configurationData
;
523 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
525 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
526 if (filename
!.trim()!.length
> 0 && event
=== 'change') {
527 delete Configuration
.configurationData
;
528 Configuration
.configurationSectionCache
.clear();
529 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
530 Configuration
.configurationChangeCallback
!().catch((error
) => {
531 throw typeof error
=== 'string' ? new Error(error
) : error
;
537 Configuration
.handleFileException(
538 Configuration
.configurationFile
,
539 FileType
.Configuration
,
540 error
as NodeJS
.ErrnoException
,
541 Configuration
.logPrefix(),
546 private static handleFileException(
549 error
: NodeJS
.ErrnoException
,
552 const prefix
= isNotEmptyString(logPrefix
) ? `${logPrefix} ` : '';
554 switch (error
.code
) {
556 logMsg
= `${fileType} file ${file} not found:`;
559 logMsg
= `${fileType} file ${file} already exists:`;
562 logMsg
= `${fileType} file ${file} access denied:`;
565 logMsg
= `${fileType} file ${file} permission denied:`;
568 logMsg
= `${fileType} file ${file} error:`;
570 console
.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
574 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
575 switch (storageType) {
576 case StorageType.JSON_FILE:
577 return Configuration.buildPerformanceUriFilePath(
578 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}
`,
580 case StorageType.SQLITE:
581 return Configuration.buildPerformanceUriFilePath(
582 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}
.db
`,
585 throw new Error(`Unsupported storage
type '${storageType}'`);
589 private static buildPerformanceUriFilePath(file: string) {
590 return `file
://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;