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
,
23 import { WorkerConstants
, WorkerProcessType
} from
'../worker';
25 type ConfigurationSectionType
=
27 | StorageConfiguration
29 | UIServerConfiguration
;
31 export class Configuration
{
32 private static configurationFile
= join(
33 dirname(fileURLToPath(import.meta
.url
)),
38 private static configurationFileWatcher
?: FSWatcher
;
39 private static configurationData
?: ConfigurationData
;
40 private static configurationSectionCache
= new Map
<
42 ConfigurationSectionType
44 [ConfigurationSection
.log
, Configuration
.buildLogSection()],
45 [ConfigurationSection
.performanceStorage
, Configuration
.buildPerformanceStorageSection()],
46 [ConfigurationSection
.worker
, Configuration
.buildWorkerSection()],
47 [ConfigurationSection
.uiServer
, Configuration
.buildUIServerSection()],
50 private static configurationChangeCallback
?: () => Promise
<void>;
52 private constructor() {
53 // This is intentional
56 public static setConfigurationChangeCallback(cb
: () => Promise
<void>): void {
57 Configuration
.configurationChangeCallback
= cb
;
60 public static getConfigurationSection
<T
extends ConfigurationSectionType
>(
61 sectionName
: ConfigurationSection
,
63 if (!Configuration
.isConfigurationSectionCached(sectionName
)) {
64 Configuration
.cacheConfigurationSection(sectionName
);
66 return Configuration
.configurationSectionCache
.get(sectionName
) as T
;
69 public static getAutoReconnectMaxRetries(): number | undefined {
70 Configuration
.warnDeprecatedConfigurationKey(
71 'autoReconnectTimeout',
73 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
75 Configuration
.warnDeprecatedConfigurationKey(
78 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
80 Configuration
.warnDeprecatedConfigurationKey(
81 'autoReconnectMaxRetries',
83 'Use it in charging station template instead',
85 if (hasOwnProp(Configuration
.getConfigurationData(), 'autoReconnectMaxRetries')) {
86 return Configuration
.getConfigurationData()?.autoReconnectMaxRetries
;
90 public static getStationTemplateUrls(): StationTemplateUrl
[] | undefined {
91 Configuration
.warnDeprecatedConfigurationKey(
92 'stationTemplateURLs',
94 "Use 'stationTemplateUrls' instead",
96 // eslint-disable-next-line @typescript-eslint/dot-notation
98 Configuration
.getConfigurationData()!['stationTemplateURLs' as keyof ConfigurationData
],
100 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
101 Configuration
.getConfigurationData()![
102 // eslint-disable-next-line @typescript-eslint/dot-notation
103 'stationTemplateURLs' as keyof ConfigurationData
104 ] as StationTemplateUrl
[]);
105 Configuration
.getConfigurationData()!.stationTemplateUrls
.forEach(
106 (stationTemplateUrl
: StationTemplateUrl
) => {
107 // eslint-disable-next-line @typescript-eslint/dot-notation
108 if (!isUndefined(stationTemplateUrl
['numberOfStation' as keyof StationTemplateUrl
])) {
110 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
111 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`,
117 return Configuration
.getConfigurationData()?.stationTemplateUrls
;
120 public static getSupervisionUrls(): string | string[] | undefined {
121 Configuration
.warnDeprecatedConfigurationKey(
124 "Use 'supervisionUrls' instead",
126 // eslint-disable-next-line @typescript-eslint/dot-notation
129 Configuration
.getConfigurationData()!['supervisionURLs' as keyof ConfigurationData
],
132 Configuration
.getConfigurationData()!.supervisionUrls
= Configuration
.getConfigurationData()![
133 // eslint-disable-next-line @typescript-eslint/dot-notation
134 'supervisionURLs' as keyof ConfigurationData
135 ] as string | string[];
137 return Configuration
.getConfigurationData()?.supervisionUrls
;
140 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
141 Configuration
.warnDeprecatedConfigurationKey(
142 'distributeStationToTenantEqually',
144 "Use 'supervisionUrlDistribution' instead",
146 Configuration
.warnDeprecatedConfigurationKey(
147 'distributeStationsToTenantsEqually',
149 "Use 'supervisionUrlDistribution' instead",
151 return hasOwnProp(Configuration
.getConfigurationData(), 'supervisionUrlDistribution')
152 ? Configuration
.getConfigurationData()?.supervisionUrlDistribution
153 : SupervisionUrlDistribution
.ROUND_ROBIN
;
156 public static workerPoolInUse(): boolean {
157 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.staticPool
].includes(
158 Configuration
.buildWorkerSection().processType
!,
162 public static workerDynamicPoolInUse(): boolean {
163 return Configuration
.buildWorkerSection().processType
=== WorkerProcessType
.dynamicPool
;
166 private static isConfigurationSectionCached(sectionName
: ConfigurationSection
): boolean {
167 return Configuration
.configurationSectionCache
.has(sectionName
);
170 private static cacheConfigurationSection(sectionName
: ConfigurationSection
): void {
171 switch (sectionName
) {
172 case ConfigurationSection
.log
:
173 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildLogSection());
175 case ConfigurationSection
.performanceStorage
:
176 Configuration
.configurationSectionCache
.set(
178 Configuration
.buildPerformanceStorageSection(),
181 case ConfigurationSection
.worker
:
182 Configuration
.configurationSectionCache
.set(
184 Configuration
.buildWorkerSection(),
187 case ConfigurationSection
.uiServer
:
188 Configuration
.configurationSectionCache
.set(
190 Configuration
.buildUIServerSection(),
194 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
195 throw new Error(`Unknown configuration section '${sectionName}'`);
199 private static buildUIServerSection(): UIServerConfiguration
{
200 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
202 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
203 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`,
207 let uiServerConfiguration
: UIServerConfiguration
= {
209 type: ApplicationProtocol
.WS
,
211 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
212 port
: Constants
.DEFAULT_UI_SERVER_PORT
,
215 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
216 uiServerConfiguration
= merge
<UIServerConfiguration
>(
217 uiServerConfiguration
,
218 Configuration
.getConfigurationData()!.uiServer
!,
221 if (isCFEnvironment() === true) {
222 delete uiServerConfiguration
.options
?.host
;
223 uiServerConfiguration
.options
!.port
= parseInt(process
.env
.PORT
!);
225 return uiServerConfiguration
;
228 private static buildPerformanceStorageSection(): StorageConfiguration
{
229 Configuration
.warnDeprecatedConfigurationKey(
231 ConfigurationSection
.performanceStorage
,
234 let storageConfiguration
: StorageConfiguration
= {
236 type: StorageType
.JSON_FILE
,
237 uri
: Configuration
.getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
239 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.performanceStorage
)) {
240 storageConfiguration
= {
241 ...storageConfiguration
,
242 ...Configuration
.getConfigurationData()?.performanceStorage
,
243 ...(Configuration
.getConfigurationData()?.performanceStorage
?.type ===
244 StorageType
.JSON_FILE
&&
245 Configuration
.getConfigurationData()?.performanceStorage
?.uri
&& {
246 uri
: Configuration
.buildPerformanceUriFilePath(
247 new URL(Configuration
.getConfigurationData()!.performanceStorage
!.uri
!).pathname
,
252 return storageConfiguration
;
255 private static buildLogSection(): LogConfiguration
{
256 Configuration
.warnDeprecatedConfigurationKey(
259 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
261 Configuration
.warnDeprecatedConfigurationKey(
264 `Use '${ConfigurationSection.log}' section to define the log file instead`,
266 Configuration
.warnDeprecatedConfigurationKey(
269 `Use '${ConfigurationSection.log}' section to define the log error file instead`,
271 Configuration
.warnDeprecatedConfigurationKey(
274 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
276 Configuration
.warnDeprecatedConfigurationKey(
277 'logStatisticsInterval',
279 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
281 Configuration
.warnDeprecatedConfigurationKey(
284 `Use '${ConfigurationSection.log}' section to define the log level instead`,
286 Configuration
.warnDeprecatedConfigurationKey(
289 `Use '${ConfigurationSection.log}' section to define the log format instead`,
291 Configuration
.warnDeprecatedConfigurationKey(
294 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
296 Configuration
.warnDeprecatedConfigurationKey(
299 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
301 Configuration
.warnDeprecatedConfigurationKey(
304 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
306 const defaultLogConfiguration
: LogConfiguration
= {
308 file
: 'logs/combined.log',
309 errorFile
: 'logs/error.log',
310 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
315 const deprecatedLogConfiguration
: LogConfiguration
= {
316 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logEnabled') && {
317 enabled
: Configuration
.getConfigurationData()?.logEnabled
,
319 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
320 file
: Configuration
.getConfigurationData()?.logFile
,
322 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
323 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
,
325 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
326 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
,
328 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
329 level
: Configuration
.getConfigurationData()?.logLevel
,
331 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
332 console
: Configuration
.getConfigurationData()?.logConsole
,
334 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
335 format
: Configuration
.getConfigurationData()?.logFormat
,
337 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
338 rotate
: Configuration
.getConfigurationData()?.logRotate
,
340 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
341 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
,
343 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
344 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
,
347 const logConfiguration
: LogConfiguration
= {
348 ...defaultLogConfiguration
,
349 ...deprecatedLogConfiguration
,
350 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
351 Configuration
.getConfigurationData()?.log
),
353 return logConfiguration
;
356 private static buildWorkerSection(): WorkerConfiguration
{
357 Configuration
.warnDeprecatedConfigurationKey(
360 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
362 Configuration
.warnDeprecatedConfigurationKey(
365 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
367 Configuration
.warnDeprecatedConfigurationKey(
370 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`,
372 Configuration
.warnDeprecatedConfigurationKey(
373 'chargingStationsPerWorker',
375 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`,
377 Configuration
.warnDeprecatedConfigurationKey(
380 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`,
382 Configuration
.warnDeprecatedConfigurationKey(
385 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`,
387 Configuration
.warnDeprecatedConfigurationKey(
390 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
392 Configuration
.warnDeprecatedConfigurationKey(
393 'workerPoolMaxSize;',
395 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
397 Configuration
.warnDeprecatedConfigurationKey(
398 'workerPoolStrategy;',
400 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`,
402 const defaultWorkerConfiguration
: WorkerConfiguration
= {
403 processType
: WorkerProcessType
.workerSet
,
404 startDelay
: WorkerConstants
.DEFAULT_WORKER_START_DELAY
,
405 elementsPerWorker
: 'auto',
406 elementStartDelay
: WorkerConstants
.DEFAULT_ELEMENT_START_DELAY
,
407 poolMinSize
: WorkerConstants
.DEFAULT_POOL_MIN_SIZE
,
408 poolMaxSize
: WorkerConstants
.DEFAULT_POOL_MAX_SIZE
,
410 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
411 delete Configuration
.getConfigurationData()?.workerPoolStrategy
;
412 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
413 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
414 processType
: Configuration
.getConfigurationData()?.workerProcess
,
416 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
417 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
,
419 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
420 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
,
422 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementStartDelay') && {
423 elementStartDelay
: Configuration
.getConfigurationData()?.elementStartDelay
,
425 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
426 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
,
428 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
429 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
,
432 Configuration
.warnDeprecatedConfigurationKey(
434 ConfigurationSection
.worker
,
435 'Not publicly exposed to end users',
437 const workerConfiguration
: WorkerConfiguration
= {
438 ...defaultWorkerConfiguration
,
439 ...deprecatedWorkerConfiguration
,
440 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
441 Configuration
.getConfigurationData()?.worker
),
443 if (!Object.values(WorkerProcessType
).includes(workerConfiguration
.processType
!)) {
444 throw new SyntaxError(
445 `Invalid worker process type '${workerConfiguration.processType}' defined in configuration`,
448 return workerConfiguration
;
451 private static logPrefix
= (): string => {
452 return `${new Date().toLocaleString()} Simulator configuration |`;
455 private static warnDeprecatedConfigurationKey(
457 sectionName
?: string,
462 !isUndefined(Configuration
.getConfigurationData()![sectionName
as keyof ConfigurationData
]) &&
465 Configuration
.getConfigurationData()![sectionName
as keyof ConfigurationData
] as Record
<
473 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
474 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
475 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
480 !isUndefined(Configuration
.getConfigurationData()![key
as keyof ConfigurationData
])
483 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
484 `Deprecated configuration key
'${key}' usage$
{
485 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
492 private static getConfigurationData(): ConfigurationData
| undefined {
493 if (!Configuration
.configurationData
) {
495 Configuration
.configurationData
= JSON
.parse(
496 readFileSync(Configuration
.configurationFile
, 'utf8'),
497 ) as ConfigurationData
;
498 if (!Configuration
.configurationFileWatcher
) {
499 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
502 Configuration
.handleFileException(
503 Configuration
.configurationFile
,
504 FileType
.Configuration
,
505 error
as NodeJS
.ErrnoException
,
506 Configuration
.logPrefix(),
510 return Configuration
.configurationData
;
513 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
515 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
516 if (filename
!.trim()!.length
> 0 && event
=== 'change') {
517 delete Configuration
.configurationData
;
518 Configuration
.configurationSectionCache
.clear();
519 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
520 Configuration
.configurationChangeCallback
!().catch((error
) => {
521 throw typeof error
=== 'string' ? new Error(error
) : error
;
527 Configuration
.handleFileException(
528 Configuration
.configurationFile
,
529 FileType
.Configuration
,
530 error
as NodeJS
.ErrnoException
,
531 Configuration
.logPrefix(),
536 private static handleFileException(
539 error
: NodeJS
.ErrnoException
,
542 const prefix
= isNotEmptyString(logPrefix
) ? `${logPrefix} ` : '';
544 switch (error
.code
) {
546 logMsg
= `${fileType} file ${file} not found:`;
549 logMsg
= `${fileType} file ${file} already exists:`;
552 logMsg
= `${fileType} file ${file} access denied:`;
555 logMsg
= `${fileType} file ${file} permission denied:`;
558 logMsg
= `${fileType} file ${file} error:`;
560 console
.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
564 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
565 switch (storageType) {
566 case StorageType.JSON_FILE:
567 return Configuration.buildPerformanceUriFilePath(
568 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}
`,
570 case StorageType.SQLITE:
571 return Configuration.buildPerformanceUriFilePath(
572 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}
.db
`,
575 throw new Error(`Performance storage URI
is mandatory
with storage
type '${storageType}'`);
579 private static buildPerformanceUriFilePath(file: string) {
580 return `file
://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;