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
.staticPool
].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(
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 Configuration
.warnDeprecatedConfigurationKey(
404 ConfigurationSection
.worker
,
405 'Not publicly exposed to end users',
408 Configuration
.warnDeprecatedConfigurationKey(
411 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
413 Configuration
.warnDeprecatedConfigurationKey(
416 `Use '${ConfigurationSection.log}' section to define the log file instead`,
418 Configuration
.warnDeprecatedConfigurationKey(
421 `Use '${ConfigurationSection.log}' section to define the log error file instead`,
423 Configuration
.warnDeprecatedConfigurationKey(
426 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
428 Configuration
.warnDeprecatedConfigurationKey(
429 'logStatisticsInterval',
431 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
433 Configuration
.warnDeprecatedConfigurationKey(
436 `Use '${ConfigurationSection.log}' section to define the log level instead`,
438 Configuration
.warnDeprecatedConfigurationKey(
441 `Use '${ConfigurationSection.log}' section to define the log format instead`,
443 Configuration
.warnDeprecatedConfigurationKey(
446 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
448 Configuration
.warnDeprecatedConfigurationKey(
451 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
453 Configuration
.warnDeprecatedConfigurationKey(
456 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
458 // performanceStorage section
459 Configuration
.warnDeprecatedConfigurationKey(
461 ConfigurationSection
.performanceStorage
,
465 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
467 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
468 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`,
474 private static warnDeprecatedConfigurationKey(
476 sectionName
?: string,
482 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
],
486 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
] as Record
<
494 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
495 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
496 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
501 !isUndefined(Configuration
.getConfigurationData()?.[key
as keyof ConfigurationData
])
504 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
505 `Deprecated configuration key
'${key}' usage$
{
506 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
513 private static getConfigurationData(): ConfigurationData
| undefined {
514 if (!Configuration
.configurationData
) {
516 Configuration
.configurationData
= JSON
.parse(
517 readFileSync(Configuration
.configurationFile
, 'utf8'),
518 ) as ConfigurationData
;
519 if (!Configuration
.configurationFileWatcher
) {
520 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
523 Configuration
.handleFileException(
524 Configuration
.configurationFile
,
525 FileType
.Configuration
,
526 error
as NodeJS
.ErrnoException
,
527 Configuration
.logPrefix(),
531 return Configuration
.configurationData
;
534 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
536 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
537 if (filename
!.trim()!.length
> 0 && event
=== 'change') {
538 delete Configuration
.configurationData
;
539 Configuration
.configurationSectionCache
.clear();
540 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
541 Configuration
.configurationChangeCallback().catch((error
) => {
542 throw typeof error
=== 'string' ? new Error(error
) : error
;
548 Configuration
.handleFileException(
549 Configuration
.configurationFile
,
550 FileType
.Configuration
,
551 error
as NodeJS
.ErrnoException
,
552 Configuration
.logPrefix(),
557 private static handleFileException(
560 error
: NodeJS
.ErrnoException
,
563 const prefix
= isNotEmptyString(logPfx
) ? `${logPfx} ` : '';
565 switch (error
.code
) {
567 logMsg
= `${fileType} file ${file} not found: `;
570 logMsg
= `${fileType} file ${file} already exists: `;
573 logMsg
= `${fileType} file ${file} access denied: `;
576 logMsg
= `${fileType} file ${file} permission denied: `;
579 logMsg
= `${fileType} file ${file} error: `;
581 console
.error(`${chalk.green(prefix)}${chalk.red(logMsg)}`, error
);
585 private static getDefaultPerformanceStorageUri(storageType
: StorageType
) {
586 switch (storageType
) {
587 case StorageType
.JSON_FILE
:
588 return Configuration
.buildPerformanceUriFilePath(
589 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}`,
591 case StorageType
.SQLITE
:
592 return Configuration
.buildPerformanceUriFilePath(
593 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`,
596 throw new Error(`Unsupported storage type '${storageType}'`);
600 private static buildPerformanceUriFilePath(file
: string) {
601 return `file://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;