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
, once
} 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 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 getStationTemplateUrls(): StationTemplateUrl
[] | undefined {
76 const checkDeprecatedConfigurationKeysOnce
= once(
77 Configuration
.checkDeprecatedConfigurationKeys
.bind(Configuration
),
80 checkDeprecatedConfigurationKeysOnce();
81 return Configuration
.getConfigurationData()?.stationTemplateUrls
;
84 public static getSupervisionUrls(): string | string[] | undefined {
87 Configuration
.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData
],
90 Configuration
.getConfigurationData()!.supervisionUrls
= Configuration
.getConfigurationData()![
91 'supervisionURLs' as keyof ConfigurationData
92 ] as string | string[];
94 return Configuration
.getConfigurationData()?.supervisionUrls
;
97 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
98 return hasOwnProp(Configuration
.getConfigurationData(), 'supervisionUrlDistribution')
99 ? Configuration
.getConfigurationData()?.supervisionUrlDistribution
100 : SupervisionUrlDistribution
.ROUND_ROBIN
;
103 public static workerPoolInUse(): boolean {
104 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.staticPool
].includes(
105 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
110 public static workerDynamicPoolInUse(): boolean {
112 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
113 .processType
=== WorkerProcessType
.dynamicPool
117 private static isConfigurationSectionCached(sectionName
: ConfigurationSection
): boolean {
118 return Configuration
.configurationSectionCache
.has(sectionName
);
121 private static cacheConfigurationSection(sectionName
: ConfigurationSection
): void {
122 switch (sectionName
) {
123 case ConfigurationSection
.log
:
124 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildLogSection());
126 case ConfigurationSection
.performanceStorage
:
127 Configuration
.configurationSectionCache
.set(
129 Configuration
.buildPerformanceStorageSection(),
132 case ConfigurationSection
.worker
:
133 Configuration
.configurationSectionCache
.set(
135 Configuration
.buildWorkerSection(),
138 case ConfigurationSection
.uiServer
:
139 Configuration
.configurationSectionCache
.set(
141 Configuration
.buildUIServerSection(),
145 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
146 throw new Error(`Unknown configuration section '${sectionName}'`);
150 private static buildUIServerSection(): UIServerConfiguration
{
151 let uiServerConfiguration
: UIServerConfiguration
= {
153 type: ApplicationProtocol
.WS
,
155 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
156 port
: Constants
.DEFAULT_UI_SERVER_PORT
,
159 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
160 uiServerConfiguration
= merge
<UIServerConfiguration
>(
161 uiServerConfiguration
,
162 Configuration
.getConfigurationData()!.uiServer
!,
165 if (isCFEnvironment() === true) {
166 delete uiServerConfiguration
.options
?.host
;
167 uiServerConfiguration
.options
!.port
= parseInt(process
.env
.PORT
!);
169 return uiServerConfiguration
;
172 private static buildPerformanceStorageSection(): StorageConfiguration
{
173 let storageConfiguration
: StorageConfiguration
= {
175 type: StorageType
.JSON_FILE
,
176 uri
: Configuration
.getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
178 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.performanceStorage
)) {
179 storageConfiguration
= {
180 ...storageConfiguration
,
181 ...Configuration
.getConfigurationData()?.performanceStorage
,
182 ...(Configuration
.getConfigurationData()?.performanceStorage
?.type ===
183 StorageType
.JSON_FILE
&&
184 Configuration
.getConfigurationData()?.performanceStorage
?.uri
&& {
185 uri
: Configuration
.buildPerformanceUriFilePath(
186 new URL(Configuration
.getConfigurationData()!.performanceStorage
!.uri
!).pathname
,
191 return storageConfiguration
;
194 private static buildLogSection(): LogConfiguration
{
195 const defaultLogConfiguration
: LogConfiguration
= {
197 file
: 'logs/combined.log',
198 errorFile
: 'logs/error.log',
199 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
204 const deprecatedLogConfiguration
: LogConfiguration
= {
205 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logEnabled') && {
206 enabled
: Configuration
.getConfigurationData()?.logEnabled
,
208 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
209 file
: Configuration
.getConfigurationData()?.logFile
,
211 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
212 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
,
214 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
215 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
,
217 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
218 level
: Configuration
.getConfigurationData()?.logLevel
,
220 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
221 console
: Configuration
.getConfigurationData()?.logConsole
,
223 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
224 format
: Configuration
.getConfigurationData()?.logFormat
,
226 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
227 rotate
: Configuration
.getConfigurationData()?.logRotate
,
229 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
230 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
,
232 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
233 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
,
236 const logConfiguration
: LogConfiguration
= {
237 ...defaultLogConfiguration
,
238 ...deprecatedLogConfiguration
,
239 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
240 Configuration
.getConfigurationData()?.log
),
242 return logConfiguration
;
245 private static buildWorkerSection(): WorkerConfiguration
{
246 const defaultWorkerConfiguration
: WorkerConfiguration
= {
247 processType
: WorkerProcessType
.workerSet
,
248 startDelay
: DEFAULT_WORKER_START_DELAY
,
249 elementsPerWorker
: 'auto',
250 elementStartDelay
: DEFAULT_ELEMENT_START_DELAY
,
251 poolMinSize
: DEFAULT_POOL_MIN_SIZE
,
252 poolMaxSize
: DEFAULT_POOL_MAX_SIZE
,
254 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
255 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
256 processType
: Configuration
.getConfigurationData()?.workerProcess
,
258 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
259 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
,
261 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
262 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
,
264 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementStartDelay') && {
265 elementStartDelay
: Configuration
.getConfigurationData()?.elementStartDelay
,
267 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
268 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
,
270 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
271 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
,
274 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
275 delete Configuration
.getConfigurationData()?.workerPoolStrategy
;
276 const workerConfiguration
: WorkerConfiguration
= {
277 ...defaultWorkerConfiguration
,
278 ...deprecatedWorkerConfiguration
,
279 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
280 Configuration
.getConfigurationData()?.worker
),
282 if (!Object.values(WorkerProcessType
).includes(workerConfiguration
.processType
!)) {
283 throw new SyntaxError(
284 `Invalid worker process type '${workerConfiguration.processType}' defined in configuration`,
287 return workerConfiguration
;
290 private static logPrefix
= (): string => {
291 return `${new Date().toLocaleString()} Simulator configuration |`;
294 private static checkDeprecatedConfigurationKeys() {
295 // connection timeout
296 Configuration
.warnDeprecatedConfigurationKey(
297 'autoReconnectTimeout',
299 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
301 Configuration
.warnDeprecatedConfigurationKey(
304 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
306 // connection retries
307 Configuration
.warnDeprecatedConfigurationKey(
308 'autoReconnectMaxRetries',
310 'Use it in charging station template instead',
312 // station template url(s)
313 Configuration
.warnDeprecatedConfigurationKey(
314 'stationTemplateURLs',
316 "Use 'stationTemplateUrls' instead",
319 Configuration
.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData
],
321 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
322 Configuration
.getConfigurationData()![
323 'stationTemplateURLs' as keyof ConfigurationData
324 ] as StationTemplateUrl
[]);
325 Configuration
.getConfigurationData()?.stationTemplateUrls
.forEach(
326 (stationTemplateUrl
: StationTemplateUrl
) => {
327 if (!isUndefined(stationTemplateUrl
?.['numberOfStation' as keyof StationTemplateUrl
])) {
329 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
330 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`,
336 // supervision url(s)
337 Configuration
.warnDeprecatedConfigurationKey(
340 "Use 'supervisionUrls' instead",
342 // supervision urls distribution
343 Configuration
.warnDeprecatedConfigurationKey(
344 'distributeStationToTenantEqually',
346 "Use 'supervisionUrlDistribution' instead",
348 Configuration
.warnDeprecatedConfigurationKey(
349 'distributeStationsToTenantsEqually',
351 "Use 'supervisionUrlDistribution' instead",
354 Configuration
.warnDeprecatedConfigurationKey(
357 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
359 Configuration
.warnDeprecatedConfigurationKey(
362 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
364 Configuration
.warnDeprecatedConfigurationKey(
367 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`,
369 Configuration
.warnDeprecatedConfigurationKey(
370 'chargingStationsPerWorker',
372 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`,
374 Configuration
.warnDeprecatedConfigurationKey(
377 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`,
379 Configuration
.warnDeprecatedConfigurationKey(
382 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`,
384 Configuration
.warnDeprecatedConfigurationKey(
387 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
389 Configuration
.warnDeprecatedConfigurationKey(
390 'workerPoolMaxSize;',
392 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
394 Configuration
.warnDeprecatedConfigurationKey(
395 'workerPoolStrategy;',
397 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`,
399 Configuration
.warnDeprecatedConfigurationKey(
401 ConfigurationSection
.worker
,
402 'Not publicly exposed to end users',
405 Configuration
.warnDeprecatedConfigurationKey(
408 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
410 Configuration
.warnDeprecatedConfigurationKey(
413 `Use '${ConfigurationSection.log}' section to define the log file instead`,
415 Configuration
.warnDeprecatedConfigurationKey(
418 `Use '${ConfigurationSection.log}' section to define the log error file instead`,
420 Configuration
.warnDeprecatedConfigurationKey(
423 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
425 Configuration
.warnDeprecatedConfigurationKey(
426 'logStatisticsInterval',
428 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
430 Configuration
.warnDeprecatedConfigurationKey(
433 `Use '${ConfigurationSection.log}' section to define the log level instead`,
435 Configuration
.warnDeprecatedConfigurationKey(
438 `Use '${ConfigurationSection.log}' section to define the log format instead`,
440 Configuration
.warnDeprecatedConfigurationKey(
443 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
445 Configuration
.warnDeprecatedConfigurationKey(
448 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
450 Configuration
.warnDeprecatedConfigurationKey(
453 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
455 // performanceStorage section
456 Configuration
.warnDeprecatedConfigurationKey(
458 ConfigurationSection
.performanceStorage
,
462 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
464 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
465 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`,
471 private static warnDeprecatedConfigurationKey(
473 sectionName
?: string,
479 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
],
483 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
] as Record
<
491 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
492 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
493 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
498 !isUndefined(Configuration
.getConfigurationData()?.[key
as keyof ConfigurationData
])
501 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
502 `Deprecated configuration key
'${key}' usage$
{
503 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
510 private static getConfigurationData(): ConfigurationData
| undefined {
511 if (!Configuration
.configurationData
) {
513 Configuration
.configurationData
= JSON
.parse(
514 readFileSync(Configuration
.configurationFile
, 'utf8'),
515 ) as ConfigurationData
;
516 if (!Configuration
.configurationFileWatcher
) {
517 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
520 Configuration
.handleFileException(
521 Configuration
.configurationFile
,
522 FileType
.Configuration
,
523 error
as NodeJS
.ErrnoException
,
524 Configuration
.logPrefix(),
528 return Configuration
.configurationData
;
531 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
533 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
534 if (filename
!.trim()!.length
> 0 && event
=== 'change') {
535 delete Configuration
.configurationData
;
536 Configuration
.configurationSectionCache
.clear();
537 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
538 Configuration
.configurationChangeCallback
!().catch((error
) => {
539 throw typeof error
=== 'string' ? new Error(error
) : error
;
545 Configuration
.handleFileException(
546 Configuration
.configurationFile
,
547 FileType
.Configuration
,
548 error
as NodeJS
.ErrnoException
,
549 Configuration
.logPrefix(),
554 private static handleFileException(
557 error
: NodeJS
.ErrnoException
,
560 const prefix
= isNotEmptyString(logPrefix
) ? `${logPrefix} ` : '';
562 switch (error
.code
) {
564 logMsg
= `${fileType} file ${file} not found:`;
567 logMsg
= `${fileType} file ${file} already exists:`;
570 logMsg
= `${fileType} file ${file} access denied:`;
573 logMsg
= `${fileType} file ${file} permission denied:`;
576 logMsg
= `${fileType} file ${file} error:`;
578 console
.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
582 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
583 switch (storageType) {
584 case StorageType.JSON_FILE:
585 return Configuration.buildPerformanceUriFilePath(
586 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}
`,
588 case StorageType.SQLITE:
589 return Configuration.buildPerformanceUriFilePath(
590 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}
.db
`,
593 throw new Error(`Unsupported storage
type '${storageType}'`);
597 private static buildPerformanceUriFilePath(file: string) {
598 return `file
://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;