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 private static configurationFile
= join(
46 dirname(fileURLToPath(import.meta
.url
)),
51 private static configurationData
?: ConfigurationData
;
52 private static configurationFileWatcher
?: FSWatcher
;
53 private static configurationSectionCache
= new Map
<
55 ConfigurationSectionType
57 [ConfigurationSection
.log
, Configuration
.buildLogSection()],
58 [ConfigurationSection
.performanceStorage
, Configuration
.buildPerformanceStorageSection()],
59 [ConfigurationSection
.worker
, Configuration
.buildWorkerSection()],
60 [ConfigurationSection
.uiServer
, Configuration
.buildUIServerSection()],
63 private static configurationChangeCallback
?: () => Promise
<void>;
65 private constructor() {
66 // This is intentional
69 public static setConfigurationChangeCallback(cb
: () => Promise
<void>): void {
70 Configuration
.configurationChangeCallback
= cb
;
73 public static getConfigurationSection
<T
extends ConfigurationSectionType
>(
74 sectionName
: ConfigurationSection
,
76 if (!Configuration
.isConfigurationSectionCached(sectionName
)) {
77 Configuration
.cacheConfigurationSection(sectionName
);
79 return Configuration
.configurationSectionCache
.get(sectionName
) as T
;
82 public static getStationTemplateUrls(): StationTemplateUrl
[] | undefined {
83 const checkDeprecatedConfigurationKeysOnce
= once(
84 Configuration
.checkDeprecatedConfigurationKeys
.bind(Configuration
),
87 checkDeprecatedConfigurationKeysOnce();
88 return Configuration
.getConfigurationData()?.stationTemplateUrls
;
91 public static getSupervisionUrls(): string | string[] | undefined {
94 Configuration
.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData
],
97 Configuration
.getConfigurationData()!.supervisionUrls
= Configuration
.getConfigurationData()![
98 'supervisionURLs' as keyof ConfigurationData
99 ] as string | string[];
101 return Configuration
.getConfigurationData()?.supervisionUrls
;
104 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
105 return hasOwnProp(Configuration
.getConfigurationData(), 'supervisionUrlDistribution')
106 ? Configuration
.getConfigurationData()?.supervisionUrlDistribution
107 : SupervisionUrlDistribution
.ROUND_ROBIN
;
110 public static workerPoolInUse(): boolean {
111 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.staticPool
].includes(
112 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
117 public static workerDynamicPoolInUse(): boolean {
119 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
120 .processType
=== WorkerProcessType
.dynamicPool
124 private static logPrefix
= (): string => {
125 return logPrefix(' Simulator configuration |');
128 private static isConfigurationSectionCached(sectionName
: ConfigurationSection
): boolean {
129 return Configuration
.configurationSectionCache
.has(sectionName
);
132 private static cacheConfigurationSection(sectionName
: ConfigurationSection
): void {
133 switch (sectionName
) {
134 case ConfigurationSection
.log
:
135 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildLogSection());
137 case ConfigurationSection
.performanceStorage
:
138 Configuration
.configurationSectionCache
.set(
140 Configuration
.buildPerformanceStorageSection(),
143 case ConfigurationSection
.worker
:
144 Configuration
.configurationSectionCache
.set(
146 Configuration
.buildWorkerSection(),
149 case ConfigurationSection
.uiServer
:
150 Configuration
.configurationSectionCache
.set(
152 Configuration
.buildUIServerSection(),
156 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
157 throw new Error(`Unknown configuration section '${sectionName}'`);
161 private static buildUIServerSection(): UIServerConfiguration
{
162 let uiServerConfiguration
: UIServerConfiguration
= {
164 type: ApplicationProtocol
.WS
,
166 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
167 port
: Constants
.DEFAULT_UI_SERVER_PORT
,
170 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
171 uiServerConfiguration
= merge
<UIServerConfiguration
>(
172 uiServerConfiguration
,
173 Configuration
.getConfigurationData()!.uiServer
!,
176 if (isCFEnvironment() === true) {
177 delete uiServerConfiguration
.options
?.host
;
178 uiServerConfiguration
.options
!.port
= parseInt(process
.env
.PORT
!);
180 return uiServerConfiguration
;
183 private static buildPerformanceStorageSection(): StorageConfiguration
{
184 let storageConfiguration
: StorageConfiguration
= {
186 type: StorageType
.JSON_FILE
,
187 uri
: Configuration
.getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
189 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.performanceStorage
)) {
190 storageConfiguration
= {
191 ...storageConfiguration
,
192 ...Configuration
.getConfigurationData()?.performanceStorage
,
193 ...(Configuration
.getConfigurationData()?.performanceStorage
?.type ===
194 StorageType
.JSON_FILE
&&
195 Configuration
.getConfigurationData()?.performanceStorage
?.uri
&& {
196 uri
: Configuration
.buildPerformanceUriFilePath(
197 new URL(Configuration
.getConfigurationData()!.performanceStorage
!.uri
!).pathname
,
202 return storageConfiguration
;
205 private static buildLogSection(): LogConfiguration
{
206 const defaultLogConfiguration
: LogConfiguration
= {
208 file
: 'logs/combined.log',
209 errorFile
: 'logs/error.log',
210 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
215 const deprecatedLogConfiguration
: LogConfiguration
= {
216 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logEnabled') && {
217 enabled
: Configuration
.getConfigurationData()?.logEnabled
,
219 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
220 file
: Configuration
.getConfigurationData()?.logFile
,
222 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
223 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
,
225 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
226 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
,
228 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
229 level
: Configuration
.getConfigurationData()?.logLevel
,
231 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
232 console
: Configuration
.getConfigurationData()?.logConsole
,
234 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
235 format
: Configuration
.getConfigurationData()?.logFormat
,
237 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
238 rotate
: Configuration
.getConfigurationData()?.logRotate
,
240 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
241 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
,
243 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
244 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
,
247 const logConfiguration
: LogConfiguration
= {
248 ...defaultLogConfiguration
,
249 ...deprecatedLogConfiguration
,
250 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
251 Configuration
.getConfigurationData()?.log
),
253 return logConfiguration
;
256 private static buildWorkerSection(): WorkerConfiguration
{
257 const defaultWorkerConfiguration
: WorkerConfiguration
= {
258 processType
: WorkerProcessType
.workerSet
,
259 startDelay
: DEFAULT_WORKER_START_DELAY
,
260 elementsPerWorker
: 'auto',
261 elementStartDelay
: DEFAULT_ELEMENT_START_DELAY
,
262 poolMinSize
: DEFAULT_POOL_MIN_SIZE
,
263 poolMaxSize
: DEFAULT_POOL_MAX_SIZE
,
265 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
266 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
267 processType
: Configuration
.getConfigurationData()?.workerProcess
,
269 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
270 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
,
272 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
273 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
,
275 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementStartDelay') && {
276 elementStartDelay
: Configuration
.getConfigurationData()?.elementStartDelay
,
278 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
279 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
,
281 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
282 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
,
285 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
286 delete Configuration
.getConfigurationData()?.workerPoolStrategy
;
287 const workerConfiguration
: WorkerConfiguration
= {
288 ...defaultWorkerConfiguration
,
289 ...deprecatedWorkerConfiguration
,
290 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
291 Configuration
.getConfigurationData()?.worker
),
293 if (!Object.values(WorkerProcessType
).includes(workerConfiguration
.processType
!)) {
294 throw new SyntaxError(
295 `Invalid worker process type '${workerConfiguration.processType}' defined in configuration`,
298 return workerConfiguration
;
301 private static checkDeprecatedConfigurationKeys() {
302 // connection timeout
303 Configuration
.warnDeprecatedConfigurationKey(
304 'autoReconnectTimeout',
306 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
308 Configuration
.warnDeprecatedConfigurationKey(
311 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
313 // connection retries
314 Configuration
.warnDeprecatedConfigurationKey(
315 'autoReconnectMaxRetries',
317 'Use it in charging station template instead',
319 // station template url(s)
320 Configuration
.warnDeprecatedConfigurationKey(
321 'stationTemplateURLs',
323 "Use 'stationTemplateUrls' instead",
326 Configuration
.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData
],
328 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
329 Configuration
.getConfigurationData()![
330 'stationTemplateURLs' as keyof ConfigurationData
331 ] as StationTemplateUrl
[]);
332 Configuration
.getConfigurationData()?.stationTemplateUrls
.forEach(
333 (stationTemplateUrl
: StationTemplateUrl
) => {
334 if (!isUndefined(stationTemplateUrl
?.['numberOfStation' as keyof StationTemplateUrl
])) {
336 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
337 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`,
343 // supervision url(s)
344 Configuration
.warnDeprecatedConfigurationKey(
347 "Use 'supervisionUrls' instead",
349 // supervision urls distribution
350 Configuration
.warnDeprecatedConfigurationKey(
351 'distributeStationToTenantEqually',
353 "Use 'supervisionUrlDistribution' instead",
355 Configuration
.warnDeprecatedConfigurationKey(
356 'distributeStationsToTenantsEqually',
358 "Use 'supervisionUrlDistribution' instead",
361 Configuration
.warnDeprecatedConfigurationKey(
364 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
366 Configuration
.warnDeprecatedConfigurationKey(
369 `Use '${ConfigurationSection.worker}' section to define the type of worker process model instead`,
371 Configuration
.warnDeprecatedConfigurationKey(
374 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`,
376 Configuration
.warnDeprecatedConfigurationKey(
377 'chargingStationsPerWorker',
379 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`,
381 Configuration
.warnDeprecatedConfigurationKey(
384 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`,
386 Configuration
.warnDeprecatedConfigurationKey(
389 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`,
391 Configuration
.warnDeprecatedConfigurationKey(
394 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
396 Configuration
.warnDeprecatedConfigurationKey(
397 'workerPoolMaxSize;',
399 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
401 Configuration
.warnDeprecatedConfigurationKey(
402 'workerPoolStrategy;',
404 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`,
406 Configuration
.warnDeprecatedConfigurationKey(
408 ConfigurationSection
.worker
,
409 'Not publicly exposed to end users',
412 Configuration
.warnDeprecatedConfigurationKey(
415 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
417 Configuration
.warnDeprecatedConfigurationKey(
420 `Use '${ConfigurationSection.log}' section to define the log file instead`,
422 Configuration
.warnDeprecatedConfigurationKey(
425 `Use '${ConfigurationSection.log}' section to define the log error file instead`,
427 Configuration
.warnDeprecatedConfigurationKey(
430 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
432 Configuration
.warnDeprecatedConfigurationKey(
433 'logStatisticsInterval',
435 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
437 Configuration
.warnDeprecatedConfigurationKey(
440 `Use '${ConfigurationSection.log}' section to define the log level instead`,
442 Configuration
.warnDeprecatedConfigurationKey(
445 `Use '${ConfigurationSection.log}' section to define the log format instead`,
447 Configuration
.warnDeprecatedConfigurationKey(
450 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
452 Configuration
.warnDeprecatedConfigurationKey(
455 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
457 Configuration
.warnDeprecatedConfigurationKey(
460 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
462 // performanceStorage section
463 Configuration
.warnDeprecatedConfigurationKey(
465 ConfigurationSection
.performanceStorage
,
469 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
471 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
472 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`,
478 private static warnDeprecatedConfigurationKey(
480 sectionName
?: string,
486 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
],
490 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
] as Record
<
498 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
499 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
500 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
505 !isUndefined(Configuration
.getConfigurationData()?.[key
as keyof ConfigurationData
])
508 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
509 `Deprecated configuration key
'${key}' usage$
{
510 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
517 private static getConfigurationData(): ConfigurationData
| undefined {
518 if (!Configuration
.configurationData
) {
520 Configuration
.configurationData
= JSON
.parse(
521 readFileSync(Configuration
.configurationFile
, 'utf8'),
522 ) as ConfigurationData
;
523 if (!Configuration
.configurationFileWatcher
) {
524 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
527 Configuration
.handleFileException(
528 Configuration
.configurationFile
,
529 FileType
.Configuration
,
530 error
as NodeJS
.ErrnoException
,
531 Configuration
.logPrefix(),
535 return Configuration
.configurationData
;
538 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
540 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
541 if (filename
!.trim()!.length
> 0 && event
=== 'change') {
542 delete Configuration
.configurationData
;
543 Configuration
.configurationSectionCache
.clear();
544 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
545 Configuration
.configurationChangeCallback
!().catch((error
) => {
546 throw typeof error
=== 'string' ? new Error(error
) : error
;
552 Configuration
.handleFileException(
553 Configuration
.configurationFile
,
554 FileType
.Configuration
,
555 error
as NodeJS
.ErrnoException
,
556 Configuration
.logPrefix(),
561 private static handleFileException(
564 error
: NodeJS
.ErrnoException
,
567 const prefix
= isNotEmptyString(logPfx
) ? `${logPfx} ` : '';
569 switch (error
.code
) {
571 logMsg
= `${fileType} file ${file} not found:`;
574 logMsg
= `${fileType} file ${file} already exists:`;
577 logMsg
= `${fileType} file ${file} access denied:`;
580 logMsg
= `${fileType} file ${file} permission denied:`;
583 logMsg
= `${fileType} file ${file} error:`;
585 console
.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
589 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
590 switch (storageType) {
591 case StorageType.JSON_FILE:
592 return Configuration.buildPerformanceUriFilePath(
593 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}
`,
595 case StorageType.SQLITE:
596 return Configuration.buildPerformanceUriFilePath(
597 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}
.db
`,
600 throw new Error(`Unsupported storage
type '${storageType}'`);
604 private static buildPerformanceUriFilePath(file: string) {
605 return `file
://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;