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';
19 type ConfigurationData
,
22 type LogConfiguration
,
23 type StationTemplateUrl
,
24 type StorageConfiguration
,
26 SupervisionUrlDistribution
,
27 type UIServerConfiguration
,
28 type WorkerConfiguration
,
31 DEFAULT_ELEMENT_START_DELAY
,
32 DEFAULT_POOL_MAX_SIZE
,
33 DEFAULT_POOL_MIN_SIZE
,
34 DEFAULT_WORKER_START_DELAY
,
38 type ConfigurationSectionType
=
40 | StorageConfiguration
42 | UIServerConfiguration
;
44 export class Configuration
{
45 public static configurationChangeCallback
: () => Promise
<void>;
47 private static configurationFile
= join(
48 dirname(fileURLToPath(import.meta
.url
)),
53 private static configurationData
?: ConfigurationData
;
54 private static configurationFileWatcher
?: FSWatcher
;
55 private static configurationSectionCache
= new Map
<
57 ConfigurationSectionType
59 [ConfigurationSection
.log
, Configuration
.buildLogSection()],
60 [ConfigurationSection
.performanceStorage
, Configuration
.buildPerformanceStorageSection()],
61 [ConfigurationSection
.worker
, Configuration
.buildWorkerSection()],
62 [ConfigurationSection
.uiServer
, Configuration
.buildUIServerSection()],
65 private constructor() {
66 // This is intentional
69 public static getConfigurationSection
<T
extends ConfigurationSectionType
>(
70 sectionName
: ConfigurationSection
,
72 if (!Configuration
.isConfigurationSectionCached(sectionName
)) {
73 Configuration
.cacheConfigurationSection(sectionName
);
75 return Configuration
.configurationSectionCache
.get(sectionName
) as T
;
78 public static getStationTemplateUrls(): StationTemplateUrl
[] | undefined {
79 const checkDeprecatedConfigurationKeysOnce
= once(
80 Configuration
.checkDeprecatedConfigurationKeys
.bind(Configuration
),
83 checkDeprecatedConfigurationKeysOnce();
84 return Configuration
.getConfigurationData()?.stationTemplateUrls
;
87 public static getSupervisionUrls(): string | string[] | undefined {
90 Configuration
.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData
],
93 Configuration
.getConfigurationData()!.supervisionUrls
= Configuration
.getConfigurationData()![
94 'supervisionURLs' as keyof ConfigurationData
95 ] as string | string[];
97 return Configuration
.getConfigurationData()?.supervisionUrls
;
100 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
101 return hasOwnProp(Configuration
.getConfigurationData(), 'supervisionUrlDistribution')
102 ? Configuration
.getConfigurationData()?.supervisionUrlDistribution
103 : SupervisionUrlDistribution
.ROUND_ROBIN
;
106 public static workerPoolInUse(): boolean {
107 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.fixedPool
].includes(
108 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
113 public static workerDynamicPoolInUse(): boolean {
115 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
116 .processType
=== WorkerProcessType
.dynamicPool
120 private static logPrefix
= (): string => {
121 return logPrefix(' Simulator configuration |');
124 private static isConfigurationSectionCached(sectionName
: ConfigurationSection
): boolean {
125 return Configuration
.configurationSectionCache
.has(sectionName
);
128 private static cacheConfigurationSection(sectionName
: ConfigurationSection
): void {
129 switch (sectionName
) {
130 case ConfigurationSection
.log
:
131 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildLogSection());
133 case ConfigurationSection
.performanceStorage
:
134 Configuration
.configurationSectionCache
.set(
136 Configuration
.buildPerformanceStorageSection(),
139 case ConfigurationSection
.worker
:
140 Configuration
.configurationSectionCache
.set(
142 Configuration
.buildWorkerSection(),
145 case ConfigurationSection
.uiServer
:
146 Configuration
.configurationSectionCache
.set(
148 Configuration
.buildUIServerSection(),
152 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
153 throw new Error(`Unknown configuration section '${sectionName}'`);
157 private static buildUIServerSection(): UIServerConfiguration
{
158 let uiServerConfiguration
: UIServerConfiguration
= {
160 type: ApplicationProtocol
.WS
,
162 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
163 port
: Constants
.DEFAULT_UI_SERVER_PORT
,
166 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
167 uiServerConfiguration
= merge
<UIServerConfiguration
>(
168 uiServerConfiguration
,
169 Configuration
.getConfigurationData()!.uiServer
!,
172 if (isCFEnvironment() === true) {
173 delete uiServerConfiguration
.options
?.host
;
174 uiServerConfiguration
.options
!.port
= parseInt(process
.env
.PORT
!);
176 return uiServerConfiguration
;
179 private static buildPerformanceStorageSection(): StorageConfiguration
{
180 let storageConfiguration
: StorageConfiguration
= {
182 type: StorageType
.JSON_FILE
,
183 uri
: Configuration
.getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
185 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.performanceStorage
)) {
186 storageConfiguration
= {
187 ...storageConfiguration
,
188 ...Configuration
.getConfigurationData()?.performanceStorage
,
189 ...(Configuration
.getConfigurationData()?.performanceStorage
?.type ===
190 StorageType
.JSON_FILE
&&
191 Configuration
.getConfigurationData()?.performanceStorage
?.uri
&& {
192 uri
: Configuration
.buildPerformanceUriFilePath(
193 new URL(Configuration
.getConfigurationData()!.performanceStorage
!.uri
!).pathname
,
198 return storageConfiguration
;
201 private static buildLogSection(): LogConfiguration
{
202 const defaultLogConfiguration
: LogConfiguration
= {
204 file
: 'logs/combined.log',
205 errorFile
: 'logs/error.log',
206 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
211 const deprecatedLogConfiguration
: LogConfiguration
= {
212 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logEnabled') && {
213 enabled
: Configuration
.getConfigurationData()?.logEnabled
,
215 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
216 file
: Configuration
.getConfigurationData()?.logFile
,
218 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
219 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
,
221 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
222 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
,
224 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
225 level
: Configuration
.getConfigurationData()?.logLevel
,
227 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
228 console
: Configuration
.getConfigurationData()?.logConsole
,
230 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
231 format
: Configuration
.getConfigurationData()?.logFormat
,
233 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
234 rotate
: Configuration
.getConfigurationData()?.logRotate
,
236 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
237 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
,
239 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
240 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
,
243 const logConfiguration
: LogConfiguration
= {
244 ...defaultLogConfiguration
,
245 ...deprecatedLogConfiguration
,
246 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
247 Configuration
.getConfigurationData()?.log
),
249 return logConfiguration
;
252 private static buildWorkerSection(): WorkerConfiguration
{
253 const defaultWorkerConfiguration
: WorkerConfiguration
= {
254 processType
: WorkerProcessType
.workerSet
,
255 startDelay
: DEFAULT_WORKER_START_DELAY
,
256 elementsPerWorker
: 'auto',
257 elementStartDelay
: DEFAULT_ELEMENT_START_DELAY
,
258 poolMinSize
: DEFAULT_POOL_MIN_SIZE
,
259 poolMaxSize
: DEFAULT_POOL_MAX_SIZE
,
261 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
262 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
263 processType
: Configuration
.getConfigurationData()?.workerProcess
,
265 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
266 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
,
268 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
269 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
,
271 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementStartDelay') && {
272 elementStartDelay
: Configuration
.getConfigurationData()?.elementStartDelay
,
274 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
275 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
,
277 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
278 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
,
281 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
282 delete Configuration
.getConfigurationData()?.workerPoolStrategy
;
283 const workerConfiguration
: WorkerConfiguration
= {
284 ...defaultWorkerConfiguration
,
285 ...deprecatedWorkerConfiguration
,
286 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
287 Configuration
.getConfigurationData()?.worker
),
289 if (!Object.values(WorkerProcessType
).includes(workerConfiguration
.processType
!)) {
290 throw new SyntaxError(
291 `Invalid worker process type '${workerConfiguration.processType}' defined in configuration`,
294 return workerConfiguration
;
297 private static checkDeprecatedConfigurationKeys() {
298 // connection timeout
299 Configuration
.warnDeprecatedConfigurationKey(
300 'autoReconnectTimeout',
302 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
304 Configuration
.warnDeprecatedConfigurationKey(
307 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
309 // connection retries
310 Configuration
.warnDeprecatedConfigurationKey(
311 'autoReconnectMaxRetries',
313 'Use it in charging station template instead',
315 // station template url(s)
316 Configuration
.warnDeprecatedConfigurationKey(
317 'stationTemplateURLs',
319 "Use 'stationTemplateUrls' instead",
322 Configuration
.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData
],
324 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
325 Configuration
.getConfigurationData()![
326 'stationTemplateURLs' as keyof ConfigurationData
327 ] as StationTemplateUrl
[]);
328 Configuration
.getConfigurationData()?.stationTemplateUrls
.forEach(
329 (stationTemplateUrl
: StationTemplateUrl
) => {
330 if (!isUndefined(stationTemplateUrl
?.['numberOfStation' as keyof StationTemplateUrl
])) {
332 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
333 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`,
339 // supervision url(s)
340 Configuration
.warnDeprecatedConfigurationKey(
343 "Use 'supervisionUrls' instead",
345 // supervision urls distribution
346 Configuration
.warnDeprecatedConfigurationKey(
347 'distributeStationToTenantEqually',
349 "Use 'supervisionUrlDistribution' instead",
351 Configuration
.warnDeprecatedConfigurationKey(
352 'distributeStationsToTenantsEqually',
354 "Use 'supervisionUrlDistribution' instead",
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(
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 Configuration
.warnDeprecatedConfigurationKey(
404 ConfigurationSection
.worker
,
405 'Not publicly exposed to end users',
408 Configuration
.getConfigurationData()?.worker
?.processType
===
409 ('staticPool' as WorkerProcessType
)
412 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
413 `Deprecated configuration
'staticPool' value usage
in worker section
'processType' field
. Use
'${WorkerProcessType.fixedPool}' value instead
`,
418 Configuration
.warnDeprecatedConfigurationKey(
421 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
423 Configuration
.warnDeprecatedConfigurationKey(
426 `Use '${ConfigurationSection.log}' section to define the log file instead`,
428 Configuration
.warnDeprecatedConfigurationKey(
431 `Use '${ConfigurationSection.log}' section to define the log error file instead`,
433 Configuration
.warnDeprecatedConfigurationKey(
436 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
438 Configuration
.warnDeprecatedConfigurationKey(
439 'logStatisticsInterval',
441 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
443 Configuration
.warnDeprecatedConfigurationKey(
446 `Use '${ConfigurationSection.log}' section to define the log level instead`,
448 Configuration
.warnDeprecatedConfigurationKey(
451 `Use '${ConfigurationSection.log}' section to define the log format instead`,
453 Configuration
.warnDeprecatedConfigurationKey(
456 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
458 Configuration
.warnDeprecatedConfigurationKey(
461 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
463 Configuration
.warnDeprecatedConfigurationKey(
466 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
468 // performanceStorage section
469 Configuration
.warnDeprecatedConfigurationKey(
471 ConfigurationSection
.performanceStorage
,
475 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
477 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
478 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`,
484 private static warnDeprecatedConfigurationKey(
486 sectionName
?: string,
492 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
],
496 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
] as Record
<
504 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
505 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
506 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
511 !isUndefined(Configuration
.getConfigurationData()?.[key
as keyof ConfigurationData
])
514 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
515 `Deprecated configuration key
'${key}' usage$
{
516 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
523 private static getConfigurationData(): ConfigurationData
| undefined {
524 if (!Configuration
.configurationData
) {
526 Configuration
.configurationData
= JSON
.parse(
527 readFileSync(Configuration
.configurationFile
, 'utf8'),
528 ) as ConfigurationData
;
529 if (!Configuration
.configurationFileWatcher
) {
530 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
533 Configuration
.handleFileException(
534 Configuration
.configurationFile
,
535 FileType
.Configuration
,
536 error
as NodeJS
.ErrnoException
,
537 Configuration
.logPrefix(),
541 return Configuration
.configurationData
;
544 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
546 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
547 if (filename
!.trim()!.length
> 0 && event
=== 'change') {
548 delete Configuration
.configurationData
;
549 Configuration
.configurationSectionCache
.clear();
550 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
551 Configuration
.configurationChangeCallback().catch((error
) => {
552 throw typeof error
=== 'string' ? new Error(error
) : error
;
558 Configuration
.handleFileException(
559 Configuration
.configurationFile
,
560 FileType
.Configuration
,
561 error
as NodeJS
.ErrnoException
,
562 Configuration
.logPrefix(),
567 private static handleFileException(
570 error
: NodeJS
.ErrnoException
,
573 const prefix
= isNotEmptyString(logPfx
) ? `${logPfx} ` : '';
575 switch (error
.code
) {
577 logMsg
= `${fileType} file ${file} not found: `;
580 logMsg
= `${fileType} file ${file} already exists: `;
583 logMsg
= `${fileType} file ${file} access denied: `;
586 logMsg
= `${fileType} file ${file} permission denied: `;
589 logMsg
= `${fileType} file ${file} error: `;
591 console
.error(`${chalk.green(prefix)}${chalk.red(logMsg)}`, error
);
595 private static getDefaultPerformanceStorageUri(storageType
: StorageType
) {
596 switch (storageType
) {
597 case StorageType
.JSON_FILE
:
598 return Configuration
.buildPerformanceUriFilePath(
599 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}`,
601 case StorageType
.SQLITE
:
602 return Configuration
.buildPerformanceUriFilePath(
603 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`,
606 throw new Error(`Unsupported storage type '${storageType}'`);
610 private static buildPerformanceUriFilePath(file
: string) {
611 return `file://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;