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 constructor() {
64 // This is intentional
67 public static set
configurationChangeCallback(cb
: () => Promise
<void>) {
68 Configuration
.configurationChangeCallback
= cb
;
71 public static getConfigurationSection
<T
extends ConfigurationSectionType
>(
72 sectionName
: ConfigurationSection
,
74 if (!Configuration
.isConfigurationSectionCached(sectionName
)) {
75 Configuration
.cacheConfigurationSection(sectionName
);
77 return Configuration
.configurationSectionCache
.get(sectionName
) as T
;
80 public static getStationTemplateUrls(): StationTemplateUrl
[] | undefined {
81 const checkDeprecatedConfigurationKeysOnce
= once(
82 Configuration
.checkDeprecatedConfigurationKeys
.bind(Configuration
),
85 checkDeprecatedConfigurationKeysOnce();
86 return Configuration
.getConfigurationData()?.stationTemplateUrls
;
89 public static getSupervisionUrls(): string | string[] | undefined {
92 Configuration
.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData
],
95 Configuration
.getConfigurationData()!.supervisionUrls
= Configuration
.getConfigurationData()![
96 'supervisionURLs' as keyof ConfigurationData
97 ] as string | string[];
99 return Configuration
.getConfigurationData()?.supervisionUrls
;
102 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
103 return hasOwnProp(Configuration
.getConfigurationData(), 'supervisionUrlDistribution')
104 ? Configuration
.getConfigurationData()?.supervisionUrlDistribution
105 : SupervisionUrlDistribution
.ROUND_ROBIN
;
108 public static workerPoolInUse(): boolean {
109 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.staticPool
].includes(
110 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
115 public static workerDynamicPoolInUse(): boolean {
117 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
118 .processType
=== WorkerProcessType
.dynamicPool
122 private static logPrefix
= (): string => {
123 return logPrefix(' Simulator configuration |');
126 private static isConfigurationSectionCached(sectionName
: ConfigurationSection
): boolean {
127 return Configuration
.configurationSectionCache
.has(sectionName
);
130 private static cacheConfigurationSection(sectionName
: ConfigurationSection
): void {
131 switch (sectionName
) {
132 case ConfigurationSection
.log
:
133 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildLogSection());
135 case ConfigurationSection
.performanceStorage
:
136 Configuration
.configurationSectionCache
.set(
138 Configuration
.buildPerformanceStorageSection(),
141 case ConfigurationSection
.worker
:
142 Configuration
.configurationSectionCache
.set(
144 Configuration
.buildWorkerSection(),
147 case ConfigurationSection
.uiServer
:
148 Configuration
.configurationSectionCache
.set(
150 Configuration
.buildUIServerSection(),
154 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
155 throw new Error(`Unknown configuration section '${sectionName}'`);
159 private static buildUIServerSection(): UIServerConfiguration
{
160 let uiServerConfiguration
: UIServerConfiguration
= {
162 type: ApplicationProtocol
.WS
,
164 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
165 port
: Constants
.DEFAULT_UI_SERVER_PORT
,
168 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
169 uiServerConfiguration
= merge
<UIServerConfiguration
>(
170 uiServerConfiguration
,
171 Configuration
.getConfigurationData()!.uiServer
!,
174 if (isCFEnvironment() === true) {
175 delete uiServerConfiguration
.options
?.host
;
176 uiServerConfiguration
.options
!.port
= parseInt(process
.env
.PORT
!);
178 return uiServerConfiguration
;
181 private static buildPerformanceStorageSection(): StorageConfiguration
{
182 let storageConfiguration
: StorageConfiguration
= {
184 type: StorageType
.JSON_FILE
,
185 uri
: Configuration
.getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
187 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.performanceStorage
)) {
188 storageConfiguration
= {
189 ...storageConfiguration
,
190 ...Configuration
.getConfigurationData()?.performanceStorage
,
191 ...(Configuration
.getConfigurationData()?.performanceStorage
?.type ===
192 StorageType
.JSON_FILE
&&
193 Configuration
.getConfigurationData()?.performanceStorage
?.uri
&& {
194 uri
: Configuration
.buildPerformanceUriFilePath(
195 new URL(Configuration
.getConfigurationData()!.performanceStorage
!.uri
!).pathname
,
200 return storageConfiguration
;
203 private static buildLogSection(): LogConfiguration
{
204 const defaultLogConfiguration
: LogConfiguration
= {
206 file
: 'logs/combined.log',
207 errorFile
: 'logs/error.log',
208 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
213 const deprecatedLogConfiguration
: LogConfiguration
= {
214 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logEnabled') && {
215 enabled
: Configuration
.getConfigurationData()?.logEnabled
,
217 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
218 file
: Configuration
.getConfigurationData()?.logFile
,
220 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
221 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
,
223 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
224 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
,
226 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
227 level
: Configuration
.getConfigurationData()?.logLevel
,
229 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
230 console
: Configuration
.getConfigurationData()?.logConsole
,
232 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
233 format
: Configuration
.getConfigurationData()?.logFormat
,
235 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
236 rotate
: Configuration
.getConfigurationData()?.logRotate
,
238 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
239 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
,
241 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
242 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
,
245 const logConfiguration
: LogConfiguration
= {
246 ...defaultLogConfiguration
,
247 ...deprecatedLogConfiguration
,
248 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
249 Configuration
.getConfigurationData()?.log
),
251 return logConfiguration
;
254 private static buildWorkerSection(): WorkerConfiguration
{
255 const defaultWorkerConfiguration
: WorkerConfiguration
= {
256 processType
: WorkerProcessType
.workerSet
,
257 startDelay
: DEFAULT_WORKER_START_DELAY
,
258 elementsPerWorker
: 'auto',
259 elementStartDelay
: DEFAULT_ELEMENT_START_DELAY
,
260 poolMinSize
: DEFAULT_POOL_MIN_SIZE
,
261 poolMaxSize
: DEFAULT_POOL_MAX_SIZE
,
263 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
264 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
265 processType
: Configuration
.getConfigurationData()?.workerProcess
,
267 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
268 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
,
270 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
271 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
,
273 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementStartDelay') && {
274 elementStartDelay
: Configuration
.getConfigurationData()?.elementStartDelay
,
276 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
277 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
,
279 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
280 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
,
283 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
284 delete Configuration
.getConfigurationData()?.workerPoolStrategy
;
285 const workerConfiguration
: WorkerConfiguration
= {
286 ...defaultWorkerConfiguration
,
287 ...deprecatedWorkerConfiguration
,
288 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
289 Configuration
.getConfigurationData()?.worker
),
291 if (!Object.values(WorkerProcessType
).includes(workerConfiguration
.processType
!)) {
292 throw new SyntaxError(
293 `Invalid worker process type '${workerConfiguration.processType}' defined in configuration`,
296 return workerConfiguration
;
299 private static checkDeprecatedConfigurationKeys() {
300 // connection timeout
301 Configuration
.warnDeprecatedConfigurationKey(
302 'autoReconnectTimeout',
304 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
306 Configuration
.warnDeprecatedConfigurationKey(
309 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead",
311 // connection retries
312 Configuration
.warnDeprecatedConfigurationKey(
313 'autoReconnectMaxRetries',
315 'Use it in charging station template instead',
317 // station template url(s)
318 Configuration
.warnDeprecatedConfigurationKey(
319 'stationTemplateURLs',
321 "Use 'stationTemplateUrls' instead",
324 Configuration
.getConfigurationData()?.['stationTemplateURLs' as keyof ConfigurationData
],
326 (Configuration
.getConfigurationData()!.stationTemplateUrls
=
327 Configuration
.getConfigurationData()![
328 'stationTemplateURLs' as keyof ConfigurationData
329 ] as StationTemplateUrl
[]);
330 Configuration
.getConfigurationData()?.stationTemplateUrls
.forEach(
331 (stationTemplateUrl
: StationTemplateUrl
) => {
332 if (!isUndefined(stationTemplateUrl
?.['numberOfStation' as keyof StationTemplateUrl
])) {
334 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
335 `Deprecated configuration key
'numberOfStation' usage
for template file
'${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use
'numberOfStations' instead
`,
341 // supervision url(s)
342 Configuration
.warnDeprecatedConfigurationKey(
345 "Use 'supervisionUrls' instead",
347 // supervision urls distribution
348 Configuration
.warnDeprecatedConfigurationKey(
349 'distributeStationToTenantEqually',
351 "Use 'supervisionUrlDistribution' instead",
353 Configuration
.warnDeprecatedConfigurationKey(
354 'distributeStationsToTenantsEqually',
356 "Use 'supervisionUrlDistribution' 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 type of worker process model instead`,
369 Configuration
.warnDeprecatedConfigurationKey(
372 `Use '${ConfigurationSection.worker}' section to define the worker start delay instead`,
374 Configuration
.warnDeprecatedConfigurationKey(
375 'chargingStationsPerWorker',
377 `Use '${ConfigurationSection.worker}' section to define the number of element(s) per worker instead`,
379 Configuration
.warnDeprecatedConfigurationKey(
382 `Use '${ConfigurationSection.worker}' section to define the worker's element start delay instead`,
384 Configuration
.warnDeprecatedConfigurationKey(
387 `Use '${ConfigurationSection.worker}' section to define the worker pool minimum size instead`,
389 Configuration
.warnDeprecatedConfigurationKey(
392 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
394 Configuration
.warnDeprecatedConfigurationKey(
395 'workerPoolMaxSize;',
397 `Use '${ConfigurationSection.worker}' section to define the worker pool maximum size instead`,
399 Configuration
.warnDeprecatedConfigurationKey(
400 'workerPoolStrategy;',
402 `Use '${ConfigurationSection.worker}' section to define the worker pool strategy instead`,
404 Configuration
.warnDeprecatedConfigurationKey(
406 ConfigurationSection
.worker
,
407 'Not publicly exposed to end users',
410 Configuration
.warnDeprecatedConfigurationKey(
413 `Use '${ConfigurationSection.log}' section to define the logging enablement instead`,
415 Configuration
.warnDeprecatedConfigurationKey(
418 `Use '${ConfigurationSection.log}' section to define the log file instead`,
420 Configuration
.warnDeprecatedConfigurationKey(
423 `Use '${ConfigurationSection.log}' section to define the log error file instead`,
425 Configuration
.warnDeprecatedConfigurationKey(
428 `Use '${ConfigurationSection.log}' section to define the console logging enablement instead`,
430 Configuration
.warnDeprecatedConfigurationKey(
431 'logStatisticsInterval',
433 `Use '${ConfigurationSection.log}' section to define the log statistics interval instead`,
435 Configuration
.warnDeprecatedConfigurationKey(
438 `Use '${ConfigurationSection.log}' section to define the log level instead`,
440 Configuration
.warnDeprecatedConfigurationKey(
443 `Use '${ConfigurationSection.log}' section to define the log format instead`,
445 Configuration
.warnDeprecatedConfigurationKey(
448 `Use '${ConfigurationSection.log}' section to define the log rotation enablement instead`,
450 Configuration
.warnDeprecatedConfigurationKey(
453 `Use '${ConfigurationSection.log}' section to define the log maximum files instead`,
455 Configuration
.warnDeprecatedConfigurationKey(
458 `Use '${ConfigurationSection.log}' section to define the log maximum size instead`,
460 // performanceStorage section
461 Configuration
.warnDeprecatedConfigurationKey(
463 ConfigurationSection
.performanceStorage
,
467 if (hasOwnProp(Configuration
.getConfigurationData(), 'uiWebSocketServer')) {
469 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
470 `Deprecated configuration section
'uiWebSocketServer' usage
. Use
'${ConfigurationSection.uiServer}' instead
`,
476 private static warnDeprecatedConfigurationKey(
478 sectionName
?: string,
484 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
],
488 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
] as Record
<
496 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
497 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
498 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
503 !isUndefined(Configuration
.getConfigurationData()?.[key
as keyof ConfigurationData
])
506 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
507 `Deprecated configuration key
'${key}' usage$
{
508 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
515 private static getConfigurationData(): ConfigurationData
| undefined {
516 if (!Configuration
.configurationData
) {
518 Configuration
.configurationData
= JSON
.parse(
519 readFileSync(Configuration
.configurationFile
, 'utf8'),
520 ) as ConfigurationData
;
521 if (!Configuration
.configurationFileWatcher
) {
522 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
525 Configuration
.handleFileException(
526 Configuration
.configurationFile
,
527 FileType
.Configuration
,
528 error
as NodeJS
.ErrnoException
,
529 Configuration
.logPrefix(),
533 return Configuration
.configurationData
;
536 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
538 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
539 if (filename
!.trim()!.length
> 0 && event
=== 'change') {
540 delete Configuration
.configurationData
;
541 Configuration
.configurationSectionCache
.clear();
542 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
543 Configuration
.configurationChangeCallback().catch((error
) => {
544 throw typeof error
=== 'string' ? new Error(error
) : error
;
550 Configuration
.handleFileException(
551 Configuration
.configurationFile
,
552 FileType
.Configuration
,
553 error
as NodeJS
.ErrnoException
,
554 Configuration
.logPrefix(),
559 private static handleFileException(
562 error
: NodeJS
.ErrnoException
,
565 const prefix
= isNotEmptyString(logPfx
) ? `${logPfx} ` : '';
567 switch (error
.code
) {
569 logMsg
= `${fileType} file ${file} not found: `;
572 logMsg
= `${fileType} file ${file} already exists: `;
575 logMsg
= `${fileType} file ${file} access denied: `;
578 logMsg
= `${fileType} file ${file} permission denied: `;
581 logMsg
= `${fileType} file ${file} error: `;
583 console
.error(`${chalk.green(prefix)}${chalk.red(logMsg)}`, error
);
587 private static getDefaultPerformanceStorageUri(storageType
: StorageType
) {
588 switch (storageType
) {
589 case StorageType
.JSON_FILE
:
590 return Configuration
.buildPerformanceUriFilePath(
591 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}`,
593 case StorageType
.SQLITE
:
594 return Configuration
.buildPerformanceUriFilePath(
595 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`,
598 throw new Error(`Unsupported storage type '${storageType}'`);
602 private static buildPerformanceUriFilePath(file
: string) {
603 return `file://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;