1 import crypto from
'node:crypto';
2 import path from
'node:path';
3 import { fileURLToPath
} from
'node:url';
5 import chalk from
'chalk';
6 import moment from
'moment';
8 import type { ChargingStation
} from
'./internal';
9 import { BaseError
} from
'../exception';
13 type BootNotificationRequest
,
16 ChargingProfileKindType
,
18 type ChargingSchedulePeriod
,
19 type ChargingStationInfo
,
20 type ChargingStationTemplate
,
24 type OCPP16BootNotificationRequest
,
25 type OCPP20BootNotificationRequest
,
38 import { WorkerProcessType
} from
'../worker';
40 const moduleName
= 'ChargingStationUtils';
42 export class ChargingStationUtils
{
43 private constructor() {
44 // This is intentional
47 public static getChargingStationId(
49 stationTemplate
: ChargingStationTemplate
51 // In case of multiple instances: add instance index to charging station id
52 const instanceIndex
= process
.env
.CF_INSTANCE_INDEX
?? 0;
53 const idSuffix
= stationTemplate
?.nameSuffix
?? '';
54 const idStr
= `000000000${index.toString()}`;
55 return stationTemplate
?.fixedName
56 ? stationTemplate
.baseName
57 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
62 public static getHashId(index
: number, stationTemplate
: ChargingStationTemplate
): string {
63 const chargingStationInfo
= {
64 chargePointModel
: stationTemplate
.chargePointModel
,
65 chargePointVendor
: stationTemplate
.chargePointVendor
,
66 ...(!Utils
.isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
67 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
69 ...(!Utils
.isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
70 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
72 ...(!Utils
.isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
73 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
75 ...(!Utils
.isUndefined(stationTemplate
.meterType
) && {
76 meterType
: stationTemplate
.meterType
,
80 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
82 `${JSON.stringify(chargingStationInfo)}${ChargingStationUtils.getChargingStationId(
90 public static checkChargingStation(chargingStation
: ChargingStation
, logPrefix
: string): boolean {
91 if (chargingStation
.started
=== false && chargingStation
.starting
=== false) {
92 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`);
98 public static getMaxNumberOfEvses(evses
: Record
<string, EvseTemplate
>): number {
102 return Object.keys(evses
).length
;
105 public static getMaxNumberOfConnectors(connectors
: Record
<string, ConnectorStatus
>): number {
109 return Object.keys(connectors
).length
;
112 public static checkTemplateMaxConnectors(
113 templateMaxConnectors
: number,
114 templateFile
: string,
117 if (templateMaxConnectors
=== 0) {
119 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
121 } else if (templateMaxConnectors
< 0) {
123 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
128 public static getConfiguredNumberOfConnectors(stationInfo
: ChargingStationInfo
): number {
129 let configuredMaxConnectors
: number;
130 if (Utils
.isNotEmptyArray(stationInfo
.numberOfConnectors
) === true) {
131 const numberOfConnectors
= stationInfo
.numberOfConnectors
as number[];
132 configuredMaxConnectors
=
133 numberOfConnectors
[Math.floor(Utils
.secureRandom() * numberOfConnectors
.length
)];
134 } else if (Utils
.isUndefined(stationInfo
.numberOfConnectors
) === false) {
135 configuredMaxConnectors
= stationInfo
.numberOfConnectors
as number;
136 } else if (stationInfo
.Connectors
&& !stationInfo
.Evses
) {
137 configuredMaxConnectors
= stationInfo
?.Connectors
[0]
138 ? ChargingStationUtils
.getMaxNumberOfConnectors(stationInfo
.Connectors
) - 1
139 : ChargingStationUtils
.getMaxNumberOfConnectors(stationInfo
.Connectors
);
140 } else if (stationInfo
.Evses
&& !stationInfo
.Connectors
) {
141 configuredMaxConnectors
= 0;
142 for (const evse
in stationInfo
.Evses
) {
146 configuredMaxConnectors
+= ChargingStationUtils
.getMaxNumberOfConnectors(
147 stationInfo
.Evses
[evse
].Connectors
151 return configuredMaxConnectors
;
154 public static checkConfiguredMaxConnectors(
155 configuredMaxConnectors
: number,
156 templateFile
: string,
159 if (configuredMaxConnectors
<= 0) {
161 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
166 public static checkStationInfoConnectorStatus(
168 connectorStatus
: ConnectorStatus
,
172 if (!Utils
.isNullOrUndefined(connectorStatus
?.status)) {
174 `${logPrefix} Charging station information from template ${templateFile} with connector ${connectorId} status configuration defined, undefine it`
176 delete connectorStatus
.status;
180 public static buildConnectorsMap(
181 connectors
: Record
<string, ConnectorStatus
>,
184 ): Map
<number, ConnectorStatus
> {
185 const connectorsMap
= new Map
<number, ConnectorStatus
>();
186 if (ChargingStationUtils
.getMaxNumberOfConnectors(connectors
) > 0) {
187 for (const connector
in connectors
) {
188 const connectorStatus
= connectors
[connector
];
189 const connectorId
= Utils
.convertToInt(connector
);
190 ChargingStationUtils
.checkStationInfoConnectorStatus(
196 connectorsMap
.set(connectorId
, Utils
.cloneObject
<ConnectorStatus
>(connectorStatus
));
200 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
203 return connectorsMap
;
206 public static initializeConnectorsMapStatus(
207 connectors
: Map
<number, ConnectorStatus
>,
210 for (const connectorId
of connectors
.keys()) {
211 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
213 `${logPrefix} Connector ${connectorId} at initialization has a transaction started: ${
214 connectors.get(connectorId)?.transactionId
220 Utils
.isNullOrUndefined(connectors
.get(connectorId
)?.transactionStarted
)
222 connectors
.get(connectorId
).availability
= AvailabilityType
.Operative
;
223 if (Utils
.isUndefined(connectors
.get(connectorId
)?.chargingProfiles
)) {
224 connectors
.get(connectorId
).chargingProfiles
= [];
228 Utils
.isNullOrUndefined(connectors
.get(connectorId
)?.transactionStarted
)
230 ChargingStationUtils
.initializeConnectorStatus(connectors
.get(connectorId
));
235 public static resetConnectorStatus(connectorStatus
: ConnectorStatus
): void {
236 connectorStatus
.idTagLocalAuthorized
= false;
237 connectorStatus
.idTagAuthorized
= false;
238 connectorStatus
.transactionRemoteStarted
= false;
239 connectorStatus
.transactionStarted
= false;
240 delete connectorStatus
?.localAuthorizeIdTag
;
241 delete connectorStatus
?.authorizeIdTag
;
242 delete connectorStatus
?.transactionId
;
243 delete connectorStatus
?.transactionIdTag
;
244 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0;
245 delete connectorStatus
?.transactionBeginMeterValue
;
248 public static createBootNotificationRequest(
249 stationInfo
: ChargingStationInfo
,
250 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
251 ): BootNotificationRequest
{
252 const ocppVersion
= stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
;
253 switch (ocppVersion
) {
254 case OCPPVersion
.VERSION_16
:
256 chargePointModel
: stationInfo
.chargePointModel
,
257 chargePointVendor
: stationInfo
.chargePointVendor
,
258 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
259 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
261 ...(!Utils
.isUndefined(stationInfo
.chargePointSerialNumber
) && {
262 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
264 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
265 firmwareVersion
: stationInfo
.firmwareVersion
,
267 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
268 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
269 ...(!Utils
.isUndefined(stationInfo
.meterSerialNumber
) && {
270 meterSerialNumber
: stationInfo
.meterSerialNumber
,
272 ...(!Utils
.isUndefined(stationInfo
.meterType
) && {
273 meterType
: stationInfo
.meterType
,
275 } as OCPP16BootNotificationRequest
;
276 case OCPPVersion
.VERSION_20
:
277 case OCPPVersion
.VERSION_201
:
281 model
: stationInfo
.chargePointModel
,
282 vendorName
: stationInfo
.chargePointVendor
,
283 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
284 firmwareVersion
: stationInfo
.firmwareVersion
,
286 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
287 serialNumber
: stationInfo
.chargeBoxSerialNumber
,
289 ...((!Utils
.isUndefined(stationInfo
.iccid
) || !Utils
.isUndefined(stationInfo
.imsi
)) && {
291 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
292 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
296 } as OCPP20BootNotificationRequest
;
300 public static workerPoolInUse(): boolean {
301 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.staticPool
].includes(
302 Configuration
.getWorker().processType
306 public static workerDynamicPoolInUse(): boolean {
307 return Configuration
.getWorker().processType
=== WorkerProcessType
.dynamicPool
;
310 public static warnTemplateKeysDeprecation(
311 templateFile
: string,
312 stationTemplate
: ChargingStationTemplate
,
315 const templateKeys
: { key
: string; deprecatedKey
: string }[] = [
316 { key
: 'supervisionUrls', deprecatedKey
: 'supervisionUrl' },
317 { key
: 'idTagsFile', deprecatedKey
: 'authorizationFile' },
319 for (const templateKey
of templateKeys
) {
320 ChargingStationUtils
.warnDeprecatedTemplateKey(
322 templateKey
.deprecatedKey
,
325 `Use '${templateKey.key}' instead`
327 ChargingStationUtils
.convertDeprecatedTemplateKey(
329 templateKey
.deprecatedKey
,
335 public static stationTemplateToStationInfo(
336 stationTemplate
: ChargingStationTemplate
337 ): ChargingStationInfo
{
338 stationTemplate
= Utils
.cloneObject(stationTemplate
);
339 delete stationTemplate
.power
;
340 delete stationTemplate
.powerUnit
;
341 delete stationTemplate
.Configuration
;
342 delete stationTemplate
.AutomaticTransactionGenerator
;
343 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
344 delete stationTemplate
.chargePointSerialNumberPrefix
;
345 delete stationTemplate
.meterSerialNumberPrefix
;
346 return stationTemplate
as unknown
as ChargingStationInfo
;
349 public static createStationInfoHash(stationInfo
: ChargingStationInfo
): void {
350 delete stationInfo
.infoHash
;
351 stationInfo
.infoHash
= crypto
352 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
353 .update(JSON
.stringify(stationInfo
))
357 public static createSerialNumber(
358 stationTemplate
: ChargingStationTemplate
,
359 stationInfo
: ChargingStationInfo
,
361 randomSerialNumberUpperCase
?: boolean;
362 randomSerialNumber
?: boolean;
364 randomSerialNumberUpperCase
: true,
365 randomSerialNumber
: true,
368 params
= params
?? {};
369 params
.randomSerialNumberUpperCase
= params
?.randomSerialNumberUpperCase
?? true;
370 params
.randomSerialNumber
= params
?.randomSerialNumber
?? true;
371 const serialNumberSuffix
= params
?.randomSerialNumber
372 ? ChargingStationUtils
.getRandomSerialNumberSuffix({
373 upperCase
: params
.randomSerialNumberUpperCase
,
376 stationInfo
.chargePointSerialNumber
= Utils
.isNotEmptyString(
377 stationTemplate
?.chargePointSerialNumberPrefix
379 ? `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`
381 stationInfo
.chargeBoxSerialNumber
= Utils
.isNotEmptyString(
382 stationTemplate
?.chargeBoxSerialNumberPrefix
384 ? `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`
386 stationInfo
.meterSerialNumber
= Utils
.isNotEmptyString(stationTemplate
?.meterSerialNumberPrefix
)
387 ? `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`
391 public static propagateSerialNumber(
392 stationTemplate
: ChargingStationTemplate
,
393 stationInfoSrc
: ChargingStationInfo
,
394 stationInfoDst
: ChargingStationInfo
396 if (!stationInfoSrc
|| !stationTemplate
) {
398 'Missing charging station template or existing configuration to propagate serial number'
401 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
402 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
403 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
404 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
405 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
406 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
407 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
408 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
409 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
412 public static getAmperageLimitationUnitDivider(stationInfo
: ChargingStationInfo
): number {
414 switch (stationInfo
.amperageLimitationUnit
) {
415 case AmpereUnits
.DECI_AMPERE
:
418 case AmpereUnits
.CENTI_AMPERE
:
421 case AmpereUnits
.MILLI_AMPERE
:
428 public static getChargingStationConnectorChargingProfilesPowerLimit(
429 chargingStation
: ChargingStation
,
431 ): number | undefined {
432 let limit
: number, matchingChargingProfile
: ChargingProfile
;
433 // Get charging profiles for connector and sort by stack level
434 const chargingProfiles
=
435 Utils
.cloneObject(chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
)?.sort(
436 (a
, b
) => b
.stackLevel
- a
.stackLevel
438 // Get profiles on connector 0
439 if (chargingStation
.getConnectorStatus(0)?.chargingProfiles
) {
440 chargingProfiles
.push(
441 ...Utils
.cloneObject(chargingStation
.getConnectorStatus(0).chargingProfiles
).sort(
442 (a
, b
) => b
.stackLevel
- a
.stackLevel
446 if (Utils
.isNotEmptyArray(chargingProfiles
)) {
447 const result
= ChargingStationUtils
.getLimitFromChargingProfiles(
449 chargingStation
.logPrefix()
451 if (!Utils
.isNullOrUndefined(result
)) {
452 limit
= result
?.limit
;
453 matchingChargingProfile
= result
?.matchingChargingProfile
;
454 switch (chargingStation
.getCurrentOutType()) {
457 matchingChargingProfile
.chargingSchedule
.chargingRateUnit
===
458 ChargingRateUnitType
.WATT
460 : ACElectricUtils
.powerTotal(
461 chargingStation
.getNumberOfPhases(),
462 chargingStation
.getVoltageOut(),
468 matchingChargingProfile
.chargingSchedule
.chargingRateUnit
===
469 ChargingRateUnitType
.WATT
471 : DCElectricUtils
.power(chargingStation
.getVoltageOut(), limit
);
473 const connectorMaximumPower
=
474 chargingStation
.getMaximumPower() / chargingStation
.powerDivider
;
475 if (limit
> connectorMaximumPower
) {
477 `${chargingStation.logPrefix()} Charging profile id ${
478 matchingChargingProfile.chargingProfileId
479 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
482 limit
= connectorMaximumPower
;
489 public static getDefaultVoltageOut(
490 currentType
: CurrentType
,
491 templateFile
: string,
494 const errMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
495 let defaultVoltageOut
: number;
496 switch (currentType
) {
498 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
501 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
504 logger
.error(`${logPrefix} ${errMsg}`);
505 throw new BaseError(errMsg
);
507 return defaultVoltageOut
;
510 public static getIdTagsFile(stationInfo
: ChargingStationInfo
): string | undefined {
512 stationInfo
.idTagsFile
&&
514 path
.resolve(path
.dirname(fileURLToPath(import.meta
.url
)), '../'),
516 path
.basename(stationInfo
.idTagsFile
)
521 private static initializeConnectorStatus(connectorStatus
: ConnectorStatus
): void {
522 connectorStatus
.availability
= AvailabilityType
.Operative
;
523 connectorStatus
.idTagLocalAuthorized
= false;
524 connectorStatus
.idTagAuthorized
= false;
525 connectorStatus
.transactionRemoteStarted
= false;
526 connectorStatus
.transactionStarted
= false;
527 connectorStatus
.energyActiveImportRegisterValue
= 0;
528 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0;
529 if (Utils
.isUndefined(connectorStatus
.chargingProfiles
)) {
530 connectorStatus
.chargingProfiles
= [];
534 private static warnDeprecatedTemplateKey(
535 template
: ChargingStationTemplate
,
537 templateFile
: string,
541 if (!Utils
.isUndefined(template
[key
])) {
542 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
543 Utils.isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
545 logger
.warn(`${logPrefix} ${logMsg}`);
546 console
.warn(chalk
.yellow(`${logMsg}`));
550 private static convertDeprecatedTemplateKey(
551 template
: ChargingStationTemplate
,
552 deprecatedKey
: string,
555 if (!Utils
.isUndefined(template
[deprecatedKey
])) {
556 template
[key
] = template
[deprecatedKey
] as unknown
;
557 delete template
[deprecatedKey
];
562 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
564 * @param chargingProfiles -
568 private static getLimitFromChargingProfiles(
569 chargingProfiles
: ChargingProfile
[],
573 matchingChargingProfile
: ChargingProfile
;
575 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
576 const currentMoment
= moment();
577 const currentDate
= new Date();
578 for (const chargingProfile
of chargingProfiles
) {
580 const chargingSchedule
= chargingProfile
.chargingSchedule
;
581 if (!chargingSchedule
?.startSchedule
) {
583 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}`
586 // Check type (recurring) and if it is already active
587 // Adjust the daily recurring schedule to today
589 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
590 chargingProfile
.recurrencyKind
=== RecurrencyKindType
.DAILY
&&
591 currentMoment
.isAfter(chargingSchedule
.startSchedule
)
593 if (!(chargingSchedule
?.startSchedule
instanceof Date)) {
595 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not a Date object in charging profile id ${chargingProfile.chargingProfileId}. Trying to convert it to a Date object`
597 chargingSchedule
.startSchedule
= new Date(chargingSchedule
.startSchedule
);
599 chargingSchedule
.startSchedule
.setFullYear(
600 currentDate
.getFullYear(),
601 currentDate
.getMonth(),
602 currentDate
.getDate()
604 // Check if the start of the schedule is yesterday
605 if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
606 chargingSchedule
.startSchedule
.setDate(currentDate
.getDate() - 1);
608 } else if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
611 // Check if the charging profile is active
613 moment(chargingSchedule
.startSchedule
)
614 .add(chargingSchedule
.duration
, 's')
615 .isAfter(currentMoment
)
617 let lastButOneSchedule
: ChargingSchedulePeriod
;
618 // Search the right schedule period
619 for (const schedulePeriod
of chargingSchedule
.chargingSchedulePeriod
) {
620 // Handling of only one period
622 chargingSchedule
.chargingSchedulePeriod
.length
=== 1 &&
623 schedulePeriod
.startPeriod
=== 0
626 limit
: schedulePeriod
.limit
,
627 matchingChargingProfile
: chargingProfile
,
629 logger
.debug(debugLogMsg
, result
);
632 // Find the right schedule period
634 moment(chargingSchedule
.startSchedule
)
635 .add(schedulePeriod
.startPeriod
, 's')
636 .isAfter(currentMoment
)
638 // Found the schedule: last but one is the correct one
640 limit
: lastButOneSchedule
.limit
,
641 matchingChargingProfile
: chargingProfile
,
643 logger
.debug(debugLogMsg
, result
);
647 lastButOneSchedule
= schedulePeriod
;
648 // Handle the last schedule period
650 schedulePeriod
.startPeriod
===
651 chargingSchedule
.chargingSchedulePeriod
[
652 chargingSchedule
.chargingSchedulePeriod
.length
- 1
656 limit
: lastButOneSchedule
.limit
,
657 matchingChargingProfile
: chargingProfile
,
659 logger
.debug(debugLogMsg
, result
);
668 private static getRandomSerialNumberSuffix(params
?: {
669 randomBytesLength
?: number;
672 const randomSerialNumberSuffix
= crypto
673 .randomBytes(params
?.randomBytesLength
?? 16)
675 if (params
?.upperCase
) {
676 return randomSerialNumberSuffix
.toUpperCase();
678 return randomSerialNumberSuffix
;