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 warnDeprecatedConfigurationKeys
= false;
58 private static configurationChangeCallback
?: () => Promise
<void>;
60 private constructor() {
61 // This is intentional
64 public static setConfigurationChangeCallback(cb
: () => Promise
<void>): void {
65 Configuration
.configurationChangeCallback
= cb
;
68 public static getConfigurationSection
<T
extends ConfigurationSectionType
>(
69 sectionName
: ConfigurationSection
,
71 if (!Configuration
.isConfigurationSectionCached(sectionName
)) {
72 Configuration
.cacheConfigurationSection(sectionName
);
74 return Configuration
.configurationSectionCache
.get(sectionName
) as T
;
77 public static getStationTemplateUrls(): StationTemplateUrl
[] | undefined {
78 const checkDeprecatedConfigurationKeysOnce
= once(
79 Configuration
.checkDeprecatedConfigurationKeys
.bind(Configuration
),
82 checkDeprecatedConfigurationKeysOnce();
83 return Configuration
.getConfigurationData()?.stationTemplateUrls
;
86 public static getSupervisionUrls(): string | string[] | undefined {
89 Configuration
.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData
],
92 Configuration
.getConfigurationData()!.supervisionUrls
= Configuration
.getConfigurationData()![
93 'supervisionURLs' as keyof ConfigurationData
94 ] as string | string[];
96 return Configuration
.getConfigurationData()?.supervisionUrls
;
99 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution
| undefined {
100 return hasOwnProp(Configuration
.getConfigurationData(), 'supervisionUrlDistribution')
101 ? Configuration
.getConfigurationData()?.supervisionUrlDistribution
102 : SupervisionUrlDistribution
.ROUND_ROBIN
;
105 public static workerPoolInUse(): boolean {
106 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.staticPool
].includes(
107 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
112 public static workerDynamicPoolInUse(): boolean {
114 Configuration
.getConfigurationSection
<WorkerConfiguration
>(ConfigurationSection
.worker
)
115 .processType
=== WorkerProcessType
.dynamicPool
119 private static isConfigurationSectionCached(sectionName
: ConfigurationSection
): boolean {
120 return Configuration
.configurationSectionCache
.has(sectionName
);
123 private static cacheConfigurationSection(sectionName
: ConfigurationSection
): void {
124 switch (sectionName
) {
125 case ConfigurationSection
.log
:
126 Configuration
.configurationSectionCache
.set(sectionName
, Configuration
.buildLogSection());
128 case ConfigurationSection
.performanceStorage
:
129 Configuration
.configurationSectionCache
.set(
131 Configuration
.buildPerformanceStorageSection(),
134 case ConfigurationSection
.worker
:
135 Configuration
.configurationSectionCache
.set(
137 Configuration
.buildWorkerSection(),
140 case ConfigurationSection
.uiServer
:
141 Configuration
.configurationSectionCache
.set(
143 Configuration
.buildUIServerSection(),
147 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
148 throw new Error(`Unknown configuration section '${sectionName}'`);
152 private static buildUIServerSection(): UIServerConfiguration
{
153 let uiServerConfiguration
: UIServerConfiguration
= {
155 type: ApplicationProtocol
.WS
,
157 host
: Constants
.DEFAULT_UI_SERVER_HOST
,
158 port
: Constants
.DEFAULT_UI_SERVER_PORT
,
161 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.uiServer
)) {
162 uiServerConfiguration
= merge
<UIServerConfiguration
>(
163 uiServerConfiguration
,
164 Configuration
.getConfigurationData()!.uiServer
!,
167 if (isCFEnvironment() === true) {
168 delete uiServerConfiguration
.options
?.host
;
169 uiServerConfiguration
.options
!.port
= parseInt(process
.env
.PORT
!);
171 return uiServerConfiguration
;
174 private static buildPerformanceStorageSection(): StorageConfiguration
{
175 let storageConfiguration
: StorageConfiguration
= {
177 type: StorageType
.JSON_FILE
,
178 uri
: Configuration
.getDefaultPerformanceStorageUri(StorageType
.JSON_FILE
),
180 if (hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.performanceStorage
)) {
181 storageConfiguration
= {
182 ...storageConfiguration
,
183 ...Configuration
.getConfigurationData()?.performanceStorage
,
184 ...(Configuration
.getConfigurationData()?.performanceStorage
?.type ===
185 StorageType
.JSON_FILE
&&
186 Configuration
.getConfigurationData()?.performanceStorage
?.uri
&& {
187 uri
: Configuration
.buildPerformanceUriFilePath(
188 new URL(Configuration
.getConfigurationData()!.performanceStorage
!.uri
!).pathname
,
193 return storageConfiguration
;
196 private static buildLogSection(): LogConfiguration
{
197 const defaultLogConfiguration
: LogConfiguration
= {
199 file
: 'logs/combined.log',
200 errorFile
: 'logs/error.log',
201 statisticsInterval
: Constants
.DEFAULT_LOG_STATISTICS_INTERVAL
,
206 const deprecatedLogConfiguration
: LogConfiguration
= {
207 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logEnabled') && {
208 enabled
: Configuration
.getConfigurationData()?.logEnabled
,
210 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFile') && {
211 file
: Configuration
.getConfigurationData()?.logFile
,
213 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logErrorFile') && {
214 errorFile
: Configuration
.getConfigurationData()?.logErrorFile
,
216 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logStatisticsInterval') && {
217 statisticsInterval
: Configuration
.getConfigurationData()?.logStatisticsInterval
,
219 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logLevel') && {
220 level
: Configuration
.getConfigurationData()?.logLevel
,
222 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logConsole') && {
223 console
: Configuration
.getConfigurationData()?.logConsole
,
225 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logFormat') && {
226 format
: Configuration
.getConfigurationData()?.logFormat
,
228 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logRotate') && {
229 rotate
: Configuration
.getConfigurationData()?.logRotate
,
231 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxFiles') && {
232 maxFiles
: Configuration
.getConfigurationData()?.logMaxFiles
,
234 ...(hasOwnProp(Configuration
.getConfigurationData(), 'logMaxSize') && {
235 maxSize
: Configuration
.getConfigurationData()?.logMaxSize
,
238 const logConfiguration
: LogConfiguration
= {
239 ...defaultLogConfiguration
,
240 ...deprecatedLogConfiguration
,
241 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.log
) &&
242 Configuration
.getConfigurationData()?.log
),
244 return logConfiguration
;
247 private static buildWorkerSection(): WorkerConfiguration
{
248 const defaultWorkerConfiguration
: WorkerConfiguration
= {
249 processType
: WorkerProcessType
.workerSet
,
250 startDelay
: DEFAULT_WORKER_START_DELAY
,
251 elementsPerWorker
: 'auto',
252 elementStartDelay
: DEFAULT_ELEMENT_START_DELAY
,
253 poolMinSize
: DEFAULT_POOL_MIN_SIZE
,
254 poolMaxSize
: DEFAULT_POOL_MAX_SIZE
,
256 const deprecatedWorkerConfiguration
: WorkerConfiguration
= {
257 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerProcess') && {
258 processType
: Configuration
.getConfigurationData()?.workerProcess
,
260 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerStartDelay') && {
261 startDelay
: Configuration
.getConfigurationData()?.workerStartDelay
,
263 ...(hasOwnProp(Configuration
.getConfigurationData(), 'chargingStationsPerWorker') && {
264 elementsPerWorker
: Configuration
.getConfigurationData()?.chargingStationsPerWorker
,
266 ...(hasOwnProp(Configuration
.getConfigurationData(), 'elementStartDelay') && {
267 elementStartDelay
: Configuration
.getConfigurationData()?.elementStartDelay
,
269 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMinSize') && {
270 poolMinSize
: Configuration
.getConfigurationData()?.workerPoolMinSize
,
272 ...(hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolMaxSize') && {
273 poolMaxSize
: Configuration
.getConfigurationData()?.workerPoolMaxSize
,
276 hasOwnProp(Configuration
.getConfigurationData(), 'workerPoolStrategy') &&
277 delete Configuration
.getConfigurationData()?.workerPoolStrategy
;
278 const workerConfiguration
: WorkerConfiguration
= {
279 ...defaultWorkerConfiguration
,
280 ...deprecatedWorkerConfiguration
,
281 ...(hasOwnProp(Configuration
.getConfigurationData(), ConfigurationSection
.worker
) &&
282 Configuration
.getConfigurationData()?.worker
),
284 if (!Object.values(WorkerProcessType
).includes(workerConfiguration
.processType
!)) {
285 throw new SyntaxError(
286 `Invalid worker process type '${workerConfiguration.processType}' defined in configuration`,
289 return workerConfiguration
;
292 private static logPrefix
= (): string => {
293 return `${new Date().toLocaleString()} Simulator configuration |`;
296 private static checkDeprecatedConfigurationKeys() {
297 if (Configuration
.warnDeprecatedConfigurationKeys
) {
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
`,
474 Configuration
.warnDeprecatedConfigurationKeys
= true;
477 private static warnDeprecatedConfigurationKey(
479 sectionName
?: string,
485 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
],
489 Configuration
.getConfigurationData()?.[sectionName
as keyof ConfigurationData
] as Record
<
497 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
498 `Deprecated configuration key
'${key}' usage
in section
'${sectionName}'$
{
499 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
504 !isUndefined(Configuration
.getConfigurationData()?.[key
as keyof ConfigurationData
])
507 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
508 `Deprecated configuration key
'${key}' usage$
{
509 logMsgToAppend
.trim().length
> 0 ? `. ${logMsgToAppend}` : ''
516 private static getConfigurationData(): ConfigurationData
| undefined {
517 if (!Configuration
.configurationData
) {
519 Configuration
.configurationData
= JSON
.parse(
520 readFileSync(Configuration
.configurationFile
, 'utf8'),
521 ) as ConfigurationData
;
522 if (!Configuration
.configurationFileWatcher
) {
523 Configuration
.configurationFileWatcher
= Configuration
.getConfigurationFileWatcher();
526 Configuration
.handleFileException(
527 Configuration
.configurationFile
,
528 FileType
.Configuration
,
529 error
as NodeJS
.ErrnoException
,
530 Configuration
.logPrefix(),
534 return Configuration
.configurationData
;
537 private static getConfigurationFileWatcher(): FSWatcher
| undefined {
539 return watch(Configuration
.configurationFile
, (event
, filename
): void => {
540 if (filename
!.trim()!.length
> 0 && event
=== 'change') {
541 delete Configuration
.configurationData
;
542 Configuration
.configurationSectionCache
.clear();
543 if (!isUndefined(Configuration
.configurationChangeCallback
)) {
544 Configuration
.configurationChangeCallback
!().catch((error
) => {
545 throw typeof error
=== 'string' ? new Error(error
) : error
;
551 Configuration
.handleFileException(
552 Configuration
.configurationFile
,
553 FileType
.Configuration
,
554 error
as NodeJS
.ErrnoException
,
555 Configuration
.logPrefix(),
560 private static handleFileException(
563 error
: NodeJS
.ErrnoException
,
566 const prefix
= isNotEmptyString(logPrefix
) ? `${logPrefix} ` : '';
568 switch (error
.code
) {
570 logMsg
= `${fileType} file ${file} not found:`;
573 logMsg
= `${fileType} file ${file} already exists:`;
576 logMsg
= `${fileType} file ${file} access denied:`;
579 logMsg
= `${fileType} file ${file} permission denied:`;
582 logMsg
= `${fileType} file ${file} error:`;
584 console
.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
588 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
589 switch (storageType) {
590 case StorageType.JSON_FILE:
591 return Configuration.buildPerformanceUriFilePath(
592 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME}
`,
594 case StorageType.SQLITE:
595 return Configuration.buildPerformanceUriFilePath(
596 `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}
/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}
.db
`,
599 throw new Error(`Unsupported storage
type '${storageType}'`);
603 private static buildPerformanceUriFilePath(file: string) {
604 return `file
://${join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), file)}`;