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 configurationData
?: ConfigurationData
;
45 private static configurationFileWatcher
?: FSWatcher
;
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 warnDeprecatedConfigurationKeys
= false;
58 private static configurationChangeCallback
?: () => Promise
<void>;
60 private constructor() {
61 // This is intentional
64 public static setConfigurationChangeCallback(cb
: () => Promise
<void>): void {
65 Configuration
.configurationChangeCallback
= cb
;
68 public static getConfigurationSection
<T
extends ConfigurationSectionType
>(
69 sectionName
: ConfigurationSection
,
71 if (!Configuration
.isConfigurationSectionCached(sectionName
)) {
72 Configuration
.cacheConfigurationSection(sectionName
);
74 return Configuration
.configurationSectionCache
.get(sectionName
) as T
;
77 public static getStationTemplateUrls(): StationTemplateUrl
[] | undefined {
78 Configuration
.checkDeprecatedConfigurationKeys();
79 return Configuration
.getConfigurationData()?.stationTemplateUrls
;
82 public static getSupervisionUrls(): string | string[] | undefined {
85 Configuration
.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData
],
88 Configuration
.getConfigurationData()!.supervisionUrls
= Configuration
.getConfigurationData()![
89 'supervisionURLs' as keyof ConfigurationData
90 ] as string | string[];
92 return Configuration
.getConfigurationData()?.supervisionUrls
;
95 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
96 return hasOwnProp(Configuration
.getConfigurationData(), 'supervisionUrlDistribution')
97 ? Configuration
.getConfigurationData()?.supervisionUrlDistribution
98 : SupervisionUrlDistribution
.ROUND_ROBIN
;
101 public static workerPoolInUse(): boolean {
102 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.staticPool
].includes(
103 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
108 public static workerDynamicPoolInUse(): boolean {
110 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
111 .processType
=== WorkerProcessType
.dynamicPool
115 private static isConfigurationSectionCached(sectionName
: ConfigurationSection
): boolean {
116 return Configuration
.configurationSectionCache
.has(sectionName
);
119 private static cacheConfigurationSection(sectionName
: ConfigurationSection
): void {
120 switch (sectionName
) {
121 case ConfigurationSection
.log
:
122 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildLogSection());
124 case ConfigurationSection
.performanceStorage
:
125 Configuration
.configurationSectionCache
.set(
127 Configuration
.buildPerformanceStorageSection(),
130 case ConfigurationSection
.worker
:
131 Configuration
.configurationSectionCache
.set(
133 Configuration
.buildWorkerSection(),
136 case ConfigurationSection
.uiServer
:
137 Configuration
.configurationSectionCache
.set(
139 Configuration
.buildUIServerSection(),
143 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
144 throw new Error(`Unknown configuration section '${sectionName}'`);
148 private static buildUIServerSection(): UIServerConfiguration
{
149 let uiServerConfiguration
: UIServerConfiguration
= {
151 type: ApplicationProtocol
.WS
,
153 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
154 port
: Constants
.DEFAULT_UI_SERVER_PORT
,
157 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
158 uiServerConfiguration
= merge
<UIServerConfiguration
>(
159 uiServerConfiguration
,
160 Configuration
.getConfigurationData()!.uiServer
!,
163 if (isCFEnvironment() === true) {
164 delete uiServerConfiguration
.options
?.host
;
165 uiServerConfiguration
.options
!.port
= parseInt(process
.env
.PORT
!);
167 return uiServerConfiguration
;
170 private static buildPerformanceStorageSection(): StorageConfiguration
{
171 let storageConfiguration
: StorageConfiguration
= {
173 type: StorageType
.JSON_FILE
,
174 uri
: Configuration
.getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
176 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.performanceStorage
)) {
177 storageConfiguration
= {
178 ...storageConfiguration
,
179 ...Configuration
.getConfigurationData()?.performanceStorage
,
180 ...(Configuration
.getConfigurationData()?.performanceStorage
?.type ===
181 StorageType
.JSON_FILE
&&
182 Configuration
.getConfigurationData()?.performanceStorage
?.uri
&& {
183 uri
: Configuration
.buildPerformanceUriFilePath(
184 new URL(Configuration
.getConfigurationData()!.performanceStorage
!.uri
!).pathname
,
189 return storageConfiguration
;
192 private static buildLogSection(): LogConfiguration
{
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
.getConfigurationData(), 'logEnabled') && {
204 enabled
: Configuration
.getConfigurationData()?.logEnabled
,
206 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
207 file
: Configuration
.getConfigurationData()?.logFile
,
209 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
210 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
,
212 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
213 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
,
215 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
216 level
: Configuration
.getConfigurationData()?.logLevel
,
218 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
219 console
: Configuration
.getConfigurationData()?.logConsole
,
221 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
222 format
: Configuration
.getConfigurationData()?.logFormat
,
224 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
225 rotate
: Configuration
.getConfigurationData()?.logRotate
,
227 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
228 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
,
230 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
231 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
,
234 const logConfiguration
: LogConfiguration
= {
235 ...defaultLogConfiguration
,
236 ...deprecatedLogConfiguration
,
237 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
238 Configuration
.getConfigurationData()?.log
),
240 return logConfiguration
;
243 private static buildWorkerSection(): WorkerConfiguration
{
244 const defaultWorkerConfiguration
: WorkerConfiguration
= {
245 processType
: WorkerProcessType
.workerSet
,
246 startDelay
: DEFAULT_WORKER_START_DELAY
,
247 elementsPerWorker
: 'auto',
248 elementStartDelay
: DEFAULT_ELEMENT_START_DELAY
,
249 poolMinSize
: DEFAULT_POOL_MIN_SIZE
,
250 poolMaxSize
: DEFAULT_POOL_MAX_SIZE
,
252 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
253 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
254 processType
: Configuration
.getConfigurationData()?.workerProcess
,
256 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
257 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
,
259 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
260 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
,
262 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementStartDelay') && {
263 elementStartDelay
: Configuration
.getConfigurationData()?.elementStartDelay
,
265 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
266 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
,
268 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
269 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
,
272 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
273 delete Configuration
.getConfigurationData()?.workerPoolStrategy
;
274 const workerConfiguration
: WorkerConfiguration
= {
275 ...defaultWorkerConfiguration
,
276 ...deprecatedWorkerConfiguration
,
277 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
278 Configuration
.getConfigurationData()?.worker
),
280 if (!Object.values(WorkerProcessType
).includes(workerConfiguration
.processType
!)) {
281 throw new SyntaxError(
282 `Invalid worker process type '${workerConfiguration.processType}' defined in configuration`,
285 return workerConfiguration
;
288 private static logPrefix
= (): string => {
289 return `${new Date().toLocaleString()} Simulator configuration |`;
292 private static checkDeprecatedConfigurationKeys() {
293 if (Configuration
.warnDeprecatedConfigurationKeys
) {
296 // connection timeout
297 Configuration
.warnDeprecatedConfigurationKey(
298 'autoReconnectTimeout',
300 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
302 Configuration
.warnDeprecatedConfigurationKey(
305 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
307 // connection retries
308 Configuration
.warnDeprecatedConfigurationKey(
309 'autoReconnectMaxRetries',
311 'Use it in charging station template instead',
313 // station template url(s)
314 Configuration
.warnDeprecatedConfigurationKey(
315 'stationTemplateURLs',
317 "Use 'stationTemplateUrls' instead",
320 Configuration
.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData
],
322 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
323 Configuration
.getConfigurationData()![
324 'stationTemplateURLs' as keyof ConfigurationData
325 ] as StationTemplateUrl
[]);
326 Configuration
.getConfigurationData()?.stationTemplateUrls
.forEach(
327 (stationTemplateUrl
: StationTemplateUrl
) => {
328 if (!isUndefined(stationTemplateUrl
?.['numberOfStation' as keyof StationTemplateUrl
])) {
330 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
331 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`,
337 // supervision url(s)
338 Configuration
.warnDeprecatedConfigurationKey(
341 "Use 'supervisionUrls' instead",
343 // supervision urls distribution
344 Configuration
.warnDeprecatedConfigurationKey(
345 'distributeStationToTenantEqually',
347 "Use 'supervisionUrlDistribution' instead",
349 Configuration
.warnDeprecatedConfigurationKey(
350 'distributeStationsToTenantsEqually',
352 "Use 'supervisionUrlDistribution' instead",
355 Configuration
.warnDeprecatedConfigurationKey(
358 `Use '${ConfigurationSection.worker}' section to define the type of worker process model 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 worker start delay instead`,
370 Configuration
.warnDeprecatedConfigurationKey(
371 'chargingStationsPerWorker',
373 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`,
375 Configuration
.warnDeprecatedConfigurationKey(
378 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`,
380 Configuration
.warnDeprecatedConfigurationKey(
383 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`,
385 Configuration
.warnDeprecatedConfigurationKey(
388 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
390 Configuration
.warnDeprecatedConfigurationKey(
391 'workerPoolMaxSize;',
393 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
395 Configuration
.warnDeprecatedConfigurationKey(
396 'workerPoolStrategy;',
398 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`,
400 Configuration
.warnDeprecatedConfigurationKey(
402 ConfigurationSection
.worker
,
403 'Not publicly exposed to end users',
406 Configuration
.warnDeprecatedConfigurationKey(
409 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
411 Configuration
.warnDeprecatedConfigurationKey(
414 `Use '${ConfigurationSection.log}' section to define the log file instead`,
416 Configuration
.warnDeprecatedConfigurationKey(
419 `Use '${ConfigurationSection.log}' section to define the log error file instead`,
421 Configuration
.warnDeprecatedConfigurationKey(
424 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
426 Configuration
.warnDeprecatedConfigurationKey(
427 'logStatisticsInterval',
429 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
431 Configuration
.warnDeprecatedConfigurationKey(
434 `Use '${ConfigurationSection.log}' section to define the log level instead`,
436 Configuration
.warnDeprecatedConfigurationKey(
439 `Use '${ConfigurationSection.log}' section to define the log format instead`,
441 Configuration
.warnDeprecatedConfigurationKey(
444 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
446 Configuration
.warnDeprecatedConfigurationKey(
449 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
451 Configuration
.warnDeprecatedConfigurationKey(
454 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
456 // performanceStorage section
457 Configuration
.warnDeprecatedConfigurationKey(
459 ConfigurationSection
.performanceStorage
,
463 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
465 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
466 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`,
470 Configuration
.warnDeprecatedConfigurationKeys
= true;
473 private static warnDeprecatedConfigurationKey(
475 sectionName
?: string,
481 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
],
485 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
] as Record
<
493 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
494 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
495 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
500 !isUndefined(Configuration
.getConfigurationData()?.[key
as keyof ConfigurationData
])
503 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
504 `Deprecated configuration key
'${key}' usage$
{
505 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
512 private static getConfigurationData(): ConfigurationData
| undefined {
513 if (!Configuration
.configurationData
) {
515 Configuration
.configurationData
= JSON
.parse(
516 readFileSync(Configuration
.configurationFile
, 'utf8'),
517 ) as ConfigurationData
;
518 if (!Configuration
.configurationFileWatcher
) {
519 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
522 Configuration
.handleFileException(
523 Configuration
.configurationFile
,
524 FileType
.Configuration
,
525 error
as NodeJS
.ErrnoException
,
526 Configuration
.logPrefix(),
530 return Configuration
.configurationData
;
533 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
535 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
536 if (filename
!.trim()!.length
> 0 && event
=== 'change') {
537 delete Configuration
.configurationData
;
538 Configuration
.configurationSectionCache
.clear();
539 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
540 Configuration
.configurationChangeCallback
!().catch((error
) => {
541 throw typeof error
=== 'string' ? new Error(error
) : error
;
547 Configuration
.handleFileException(
548 Configuration
.configurationFile
,
549 FileType
.Configuration
,
550 error
as NodeJS
.ErrnoException
,
551 Configuration
.logPrefix(),
556 private static handleFileException(
559 error
: NodeJS
.ErrnoException
,
562 const prefix
= isNotEmptyString(logPrefix
) ? `${logPrefix} ` : '';
564 switch (error
.code
) {
566 logMsg
= `${fileType} file ${file} not found:`;
569 logMsg
= `${fileType} file ${file} already exists:`;
572 logMsg
= `${fileType} file ${file} access denied:`;
575 logMsg
= `${fileType} file ${file} permission denied:`;
578 logMsg
= `${fileType} file ${file} error:`;
580 console
.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
584 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
585 switch (storageType) {
586 case StorageType.JSON_FILE:
587 return Configuration.buildPerformanceUriFilePath(
588 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}
`,
590 case StorageType.SQLITE:
591 return Configuration.buildPerformanceUriFilePath(
592 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}
.db
`,
595 throw new Error(`Unsupported storage
type '${storageType}'`);
599 private static buildPerformanceUriFilePath(file: string) {
600 return `file
://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;