1 import { createHash
, randomBytes
} from
'node:crypto';
2 import type { EventEmitter
} from
'node:events';
3 import { basename
, dirname
, join
} from
'node:path';
4 import { env
} from
'node:process';
5 import { fileURLToPath
} from
'node:url';
7 import chalk from
'chalk';
24 import type { ChargingStation
} from
'./ChargingStation';
25 import { getConfigurationKey
} from
'./ConfigurationKeyUtils';
26 import { BaseError
} from
'../exception';
30 type BootNotificationRequest
,
33 ChargingProfileKindType
,
35 type ChargingSchedulePeriod
,
36 type ChargingStationInfo
,
37 type ChargingStationTemplate
,
38 ChargingStationWorkerMessageEvents
,
39 ConnectorPhaseRotation
,
44 type OCPP16BootNotificationRequest
,
45 type OCPP20BootNotificationRequest
,
49 ReservationTerminationReason
,
50 StandardParametersKey
,
51 SupportedFeatureProfiles
,
73 const moduleName
= 'Helpers';
75 export const getChargingStationId
= (
77 stationTemplate
: ChargingStationTemplate
| undefined,
79 if (stationTemplate
=== undefined) {
80 return "Unknown 'chargingStationId'";
82 // In case of multiple instances: add instance index to charging station id
83 const instanceIndex
= env
.CF_INSTANCE_INDEX
?? 0;
84 const idSuffix
= stationTemplate
?.nameSuffix
?? '';
85 const idStr
= `000000000${index.toString()}`;
86 return stationTemplate
?.fixedName
87 ? stationTemplate
.baseName
88 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
93 export const hasReservationExpired
= (reservation
: Reservation
): boolean => {
94 return isPast(reservation
.expiryDate
);
97 export const removeExpiredReservations
= async (
98 chargingStation
: ChargingStation
,
100 if (chargingStation
.hasEvses
) {
101 for (const evseStatus
of chargingStation
.evses
.values()) {
102 for (const connectorStatus
of evseStatus
.connectors
.values()) {
103 if (connectorStatus
.reservation
&& hasReservationExpired(connectorStatus
.reservation
)) {
104 await chargingStation
.removeReservation(
105 connectorStatus
.reservation
,
106 ReservationTerminationReason
.EXPIRED
,
112 for (const connectorStatus
of chargingStation
.connectors
.values()) {
113 if (connectorStatus
.reservation
&& hasReservationExpired(connectorStatus
.reservation
)) {
114 await chargingStation
.removeReservation(
115 connectorStatus
.reservation
,
116 ReservationTerminationReason
.EXPIRED
,
123 export const getNumberOfReservableConnectors
= (
124 connectors
: Map
<number, ConnectorStatus
>,
126 let numberOfReservableConnectors
= 0;
127 for (const [connectorId
, connectorStatus
] of connectors
) {
128 if (connectorId
=== 0) {
131 if (connectorStatus
.status === ConnectorStatusEnum
.Available
) {
132 ++numberOfReservableConnectors
;
135 return numberOfReservableConnectors
;
138 export const getHashId
= (index
: number, stationTemplate
: ChargingStationTemplate
): string => {
139 const chargingStationInfo
= {
140 chargePointModel
: stationTemplate
.chargePointModel
,
141 chargePointVendor
: stationTemplate
.chargePointVendor
,
142 ...(!isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
143 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
145 ...(!isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
146 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
148 ...(!isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
149 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
151 ...(!isUndefined(stationTemplate
.meterType
) && {
152 meterType
: stationTemplate
.meterType
,
155 return createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
156 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
160 export const checkChargingStation
= (
161 chargingStation
: ChargingStation
,
164 if (chargingStation
.started
=== false && chargingStation
.starting
=== false) {
165 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`);
171 export const getPhaseRotationValue
= (
173 numberOfPhases
: number,
174 ): string | undefined => {
176 if (connectorId
=== 0 && numberOfPhases
=== 0) {
177 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
178 } else if (connectorId
> 0 && numberOfPhases
=== 0) {
179 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
181 } else if (connectorId
>= 0 && numberOfPhases
=== 1) {
182 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
183 } else if (connectorId
>= 0 && numberOfPhases
=== 3) {
184 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
188 export const getMaxNumberOfEvses
= (evses
: Record
<string, EvseTemplate
>): number => {
192 return Object.keys(evses
).length
;
195 const getMaxNumberOfConnectors
= (connectors
: Record
<string, ConnectorStatus
>): number => {
199 return Object.keys(connectors
).length
;
202 export const getBootConnectorStatus
= (
203 chargingStation
: ChargingStation
,
205 connectorStatus
: ConnectorStatus
,
206 ): ConnectorStatusEnum
=> {
207 let connectorBootStatus
: ConnectorStatusEnum
;
209 !connectorStatus
?.status &&
210 (chargingStation
.isChargingStationAvailable() === false ||
211 chargingStation
.isConnectorAvailable(connectorId
) === false)
213 connectorBootStatus
= ConnectorStatusEnum
.Unavailable
;
214 } else if (!connectorStatus
?.status && connectorStatus
?.bootStatus
) {
215 // Set boot status in template at startup
216 connectorBootStatus
= connectorStatus
?.bootStatus
;
217 } else if (connectorStatus
?.status) {
218 // Set previous status at startup
219 connectorBootStatus
= connectorStatus
?.status;
221 // Set default status
222 connectorBootStatus
= ConnectorStatusEnum
.Available
;
224 return connectorBootStatus
;
227 export const checkTemplate
= (
228 stationTemplate
: ChargingStationTemplate
,
230 templateFile
: string,
232 if (isNullOrUndefined(stationTemplate
)) {
233 const errorMsg
= `Failed to read charging station template file ${templateFile}`;
234 logger
.error(`${logPrefix} ${errorMsg}`);
235 throw new BaseError(errorMsg
);
237 if (isEmptyObject(stationTemplate
)) {
238 const errorMsg
= `Empty charging station information from template file ${templateFile}`;
239 logger
.error(`${logPrefix} ${errorMsg}`);
240 throw new BaseError(errorMsg
);
242 if (isEmptyObject(stationTemplate
.AutomaticTransactionGenerator
!)) {
243 stationTemplate
.AutomaticTransactionGenerator
= Constants
.DEFAULT_ATG_CONFIGURATION
;
245 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
246 Constants
.DEFAULT_ATG_CONFIGURATION
,
249 if (isNullOrUndefined(stationTemplate
.idTagsFile
) || isEmptyString(stationTemplate
.idTagsFile
)) {
251 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`,
256 export const checkConnectorsConfiguration
= (
257 stationTemplate
: ChargingStationTemplate
,
259 templateFile
: string,
261 configuredMaxConnectors
: number;
262 templateMaxConnectors
: number;
263 templateMaxAvailableConnectors
: number;
265 const configuredMaxConnectors
= getConfiguredMaxNumberOfConnectors(stationTemplate
);
266 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
);
267 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
!);
268 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
);
269 const templateMaxAvailableConnectors
= stationTemplate
.Connectors
?.[0]
270 ? templateMaxConnectors
- 1
271 : templateMaxConnectors
;
273 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
274 !stationTemplate
?.randomConnectors
277 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`,
279 stationTemplate
.randomConnectors
= true;
281 return { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
};
284 export const checkStationInfoConnectorStatus
= (
286 connectorStatus
: ConnectorStatus
,
288 templateFile
: string,
290 if (!isNullOrUndefined(connectorStatus
?.status)) {
292 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`,
294 delete connectorStatus
.status;
298 export const buildConnectorsMap
= (
299 connectors
: Record
<string, ConnectorStatus
>,
301 templateFile
: string,
302 ): Map
<number, ConnectorStatus
> => {
303 const connectorsMap
= new Map
<number, ConnectorStatus
>();
304 if (getMaxNumberOfConnectors(connectors
) > 0) {
305 for (const connector
in connectors
) {
306 const connectorStatus
= connectors
[connector
];
307 const connectorId
= convertToInt(connector
);
308 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
);
309 connectorsMap
.set(connectorId
, cloneObject
<ConnectorStatus
>(connectorStatus
));
313 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`,
316 return connectorsMap
;
319 export const initializeConnectorsMapStatus
= (
320 connectors
: Map
<number, ConnectorStatus
>,
323 for (const connectorId
of connectors
.keys()) {
324 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
326 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
331 if (connectorId
=== 0) {
332 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
;
333 if (isUndefined(connectors
.get(connectorId
)?.chargingProfiles
)) {
334 connectors
.get(connectorId
)!.chargingProfiles
= [];
338 isNullOrUndefined(connectors
.get(connectorId
)?.transactionStarted
)
340 initializeConnectorStatus(connectors
.get(connectorId
)!);
345 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
346 connectorStatus
.chargingProfiles
=
347 connectorStatus
.transactionId
&& isNotEmptyArray(connectorStatus
.chargingProfiles
)
348 ? connectorStatus
.chargingProfiles
?.filter(
349 (chargingProfile
) => chargingProfile
.transactionId
!== connectorStatus
.transactionId
,
352 connectorStatus
.idTagLocalAuthorized
= false;
353 connectorStatus
.idTagAuthorized
= false;
354 connectorStatus
.transactionRemoteStarted
= false;
355 connectorStatus
.transactionStarted
= false;
356 delete connectorStatus
?.transactionStart
;
357 delete connectorStatus
?.transactionId
;
358 delete connectorStatus
?.localAuthorizeIdTag
;
359 delete connectorStatus
?.authorizeIdTag
;
360 delete connectorStatus
?.transactionIdTag
;
361 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0;
362 delete connectorStatus
?.transactionBeginMeterValue
;
365 export const createBootNotificationRequest
= (
366 stationInfo
: ChargingStationInfo
,
367 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
,
368 ): BootNotificationRequest
=> {
369 const ocppVersion
= stationInfo
.ocppVersion
!;
370 switch (ocppVersion
) {
371 case OCPPVersion
.VERSION_16
:
373 chargePointModel
: stationInfo
.chargePointModel
,
374 chargePointVendor
: stationInfo
.chargePointVendor
,
375 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
376 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
378 ...(!isUndefined(stationInfo
.chargePointSerialNumber
) && {
379 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
381 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
382 firmwareVersion
: stationInfo
.firmwareVersion
,
384 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
385 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
386 ...(!isUndefined(stationInfo
.meterSerialNumber
) && {
387 meterSerialNumber
: stationInfo
.meterSerialNumber
,
389 ...(!isUndefined(stationInfo
.meterType
) && {
390 meterType
: stationInfo
.meterType
,
392 } as OCPP16BootNotificationRequest
;
393 case OCPPVersion
.VERSION_20
:
394 case OCPPVersion
.VERSION_201
:
398 model
: stationInfo
.chargePointModel
,
399 vendorName
: stationInfo
.chargePointVendor
,
400 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
401 firmwareVersion
: stationInfo
.firmwareVersion
,
403 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
404 serialNumber
: stationInfo
.chargeBoxSerialNumber
,
406 ...((!isUndefined(stationInfo
.iccid
) || !isUndefined(stationInfo
.imsi
)) && {
408 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
409 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
413 } as OCPP20BootNotificationRequest
;
417 export const warnTemplateKeysDeprecation
= (
418 stationTemplate
: ChargingStationTemplate
,
420 templateFile
: string,
422 const templateKeys
: { deprecatedKey
: string; key
?: string }[] = [
423 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
424 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
425 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
426 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' },
428 for (const templateKey
of templateKeys
) {
429 warnDeprecatedTemplateKey(
431 templateKey
.deprecatedKey
,
434 !isUndefined(templateKey
.key
) ? `Use '${templateKey.key}' instead` : undefined,
436 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
);
440 export const stationTemplateToStationInfo
= (
441 stationTemplate
: ChargingStationTemplate
,
442 ): ChargingStationInfo
=> {
443 stationTemplate
= cloneObject
<ChargingStationTemplate
>(stationTemplate
);
444 delete stationTemplate
.power
;
445 delete stationTemplate
.powerUnit
;
446 delete stationTemplate
.Connectors
;
447 delete stationTemplate
.Evses
;
448 delete stationTemplate
.Configuration
;
449 delete stationTemplate
.AutomaticTransactionGenerator
;
450 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
451 delete stationTemplate
.chargePointSerialNumberPrefix
;
452 delete stationTemplate
.meterSerialNumberPrefix
;
453 return stationTemplate
as ChargingStationInfo
;
456 export const createSerialNumber
= (
457 stationTemplate
: ChargingStationTemplate
,
458 stationInfo
: ChargingStationInfo
,
460 randomSerialNumberUpperCase
?: boolean;
461 randomSerialNumber
?: boolean;
464 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
};
465 const serialNumberSuffix
= params
?.randomSerialNumber
466 ? getRandomSerialNumberSuffix({
467 upperCase
: params
.randomSerialNumberUpperCase
,
470 isNotEmptyString(stationTemplate
?.chargePointSerialNumberPrefix
) &&
471 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
472 isNotEmptyString(stationTemplate
?.chargeBoxSerialNumberPrefix
) &&
473 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
474 isNotEmptyString(stationTemplate
?.meterSerialNumberPrefix
) &&
475 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
478 export const propagateSerialNumber
= (
479 stationTemplate
: ChargingStationTemplate
,
480 stationInfoSrc
: ChargingStationInfo
,
481 stationInfoDst
: ChargingStationInfo
,
483 if (!stationInfoSrc
|| !stationTemplate
) {
485 'Missing charging station template or existing configuration to propagate serial number',
488 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
489 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
490 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
491 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
492 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
493 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
494 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
495 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
496 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
499 export const hasFeatureProfile
= (
500 chargingStation
: ChargingStation
,
501 featureProfile
: SupportedFeatureProfiles
,
502 ): boolean | undefined => {
503 return getConfigurationKey(
505 StandardParametersKey
.SupportedFeatureProfiles
,
506 )?.value
?.includes(featureProfile
);
509 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
511 switch (stationInfo
.amperageLimitationUnit
) {
512 case AmpereUnits
.DECI_AMPERE
:
515 case AmpereUnits
.CENTI_AMPERE
:
518 case AmpereUnits
.MILLI_AMPERE
:
526 * Gets the connector cloned charging profiles applying a power limitation
527 * and sorted by connector id descending then stack level descending
529 * @param chargingStation -
530 * @param connectorId -
531 * @returns connector charging profiles array
533 export const getConnectorChargingProfiles
= (
534 chargingStation
: ChargingStation
,
537 return cloneObject
<ChargingProfile
[]>(
538 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
539 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
541 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? []).sort(
542 (a
, b
) => b
.stackLevel
- a
.stackLevel
,
548 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
549 chargingStation
: ChargingStation
,
551 ): number | undefined => {
552 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined;
553 // Get charging profiles sorted by connector id then stack level
554 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
);
555 if (isNotEmptyArray(chargingProfiles
)) {
556 const result
= getLimitFromChargingProfiles(
560 chargingStation
.logPrefix(),
562 if (!isNullOrUndefined(result
)) {
563 limit
= result
?.limit
;
564 chargingProfile
= result
?.chargingProfile
;
565 switch (chargingStation
.stationInfo
?.currentOutType
) {
568 chargingProfile
?.chargingSchedule
?.chargingRateUnit
=== ChargingRateUnitType
.WATT
570 : ACElectricUtils
.powerTotal(
571 chargingStation
.getNumberOfPhases(),
572 chargingStation
.stationInfo
.voltageOut
!,
578 chargingProfile
?.chargingSchedule
?.chargingRateUnit
=== ChargingRateUnitType
.WATT
580 : DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
!);
582 const connectorMaximumPower
=
583 chargingStation
.stationInfo
.maximumPower
! / chargingStation
.powerDivider
;
584 if (limit
! > connectorMaximumPower
) {
586 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${chargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
589 limit
= connectorMaximumPower
;
596 export const getDefaultVoltageOut
= (
597 currentType
: CurrentType
,
599 templateFile
: string,
601 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
602 let defaultVoltageOut
: number;
603 switch (currentType
) {
605 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
608 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
611 logger
.error(`${logPrefix} ${errorMsg}`);
612 throw new BaseError(errorMsg
);
614 return defaultVoltageOut
;
617 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
619 stationInfo
.idTagsFile
&&
620 join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
624 export const waitChargingStationEvents
= async (
625 emitter
: EventEmitter
,
626 event
: ChargingStationWorkerMessageEvents
,
627 eventsToWait
: number,
628 ): Promise
<number> => {
629 return new Promise
<number>((resolve
) => {
631 if (eventsToWait
=== 0) {
634 emitter
.on(event
, () => {
636 if (events
=== eventsToWait
) {
643 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
644 let configuredMaxNumberOfConnectors
= 0;
645 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
) === true) {
646 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
647 configuredMaxNumberOfConnectors
=
648 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)];
649 } else if (isUndefined(stationTemplate
.numberOfConnectors
) === false) {
650 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
as number;
651 } else if (stationTemplate
.Connectors
&& !stationTemplate
.Evses
) {
652 configuredMaxNumberOfConnectors
= stationTemplate
.Connectors
?.[0]
653 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
654 : getMaxNumberOfConnectors(stationTemplate
.Connectors
);
655 } else if (stationTemplate
.Evses
&& !stationTemplate
.Connectors
) {
656 for (const evse
in stationTemplate
.Evses
) {
660 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
661 stationTemplate
.Evses
[evse
].Connectors
,
665 return configuredMaxNumberOfConnectors
;
668 const checkConfiguredMaxConnectors
= (
669 configuredMaxConnectors
: number,
671 templateFile
: string,
673 if (configuredMaxConnectors
<= 0) {
675 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
680 const checkTemplateMaxConnectors
= (
681 templateMaxConnectors
: number,
683 templateFile
: string,
685 if (templateMaxConnectors
=== 0) {
687 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
689 } else if (templateMaxConnectors
< 0) {
691 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
696 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
697 connectorStatus
.availability
= AvailabilityType
.Operative
;
698 connectorStatus
.idTagLocalAuthorized
= false;
699 connectorStatus
.idTagAuthorized
= false;
700 connectorStatus
.transactionRemoteStarted
= false;
701 connectorStatus
.transactionStarted
= false;
702 connectorStatus
.energyActiveImportRegisterValue
= 0;
703 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0;
704 if (isUndefined(connectorStatus
.chargingProfiles
)) {
705 connectorStatus
.chargingProfiles
= [];
709 const warnDeprecatedTemplateKey
= (
710 template
: ChargingStationTemplate
,
713 templateFile
: string,
716 if (!isUndefined(template
?.[key
as keyof ChargingStationTemplate
])) {
717 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
718 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
720 logger
.warn(`${logPrefix} ${logMsg}`);
721 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`);
725 const convertDeprecatedTemplateKey
= (
726 template
: ChargingStationTemplate
,
727 deprecatedKey
: string,
730 if (!isUndefined(template
?.[deprecatedKey
as keyof ChargingStationTemplate
])) {
731 if (!isUndefined(key
)) {
732 (template
as unknown
as Record
<string, unknown
>)[key
!] =
733 template
[deprecatedKey
as keyof ChargingStationTemplate
];
735 delete template
[deprecatedKey
as keyof ChargingStationTemplate
];
739 interface ChargingProfilesLimit
{
741 chargingProfile
: ChargingProfile
;
745 * Charging profiles shall already be sorted by connector id descending then stack level descending
747 * @param chargingStation -
748 * @param connectorId -
749 * @param chargingProfiles -
751 * @returns ChargingProfilesLimit
753 const getLimitFromChargingProfiles
= (
754 chargingStation
: ChargingStation
,
756 chargingProfiles
: ChargingProfile
[],
758 ): ChargingProfilesLimit
| undefined => {
759 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
760 const currentDate
= new Date();
761 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)!;
762 for (const chargingProfile
of chargingProfiles
) {
763 const chargingSchedule
= chargingProfile
.chargingSchedule
;
764 if (isNullOrUndefined(chargingSchedule
?.startSchedule
) && connectorStatus
?.transactionStarted
) {
766 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`,
768 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
769 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
;
772 !isNullOrUndefined(chargingSchedule
?.startSchedule
) &&
773 !isDate(chargingSchedule
?.startSchedule
)
776 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`,
778 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
?.startSchedule
)!;
781 !isNullOrUndefined(chargingSchedule
?.startSchedule
) &&
782 isNullOrUndefined(chargingSchedule
?.duration
)
785 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`,
787 // OCPP specifies that if duration is not defined, it should be infinite
788 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
!);
790 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
793 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
796 // Check if the charging profile is active
798 isWithinInterval(currentDate
, {
799 start
: chargingSchedule
.startSchedule
!,
800 end
: addSeconds(chargingSchedule
.startSchedule
!, chargingSchedule
.duration
!),
803 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
804 const chargingSchedulePeriodCompareFn
= (
805 a
: ChargingSchedulePeriod
,
806 b
: ChargingSchedulePeriod
,
807 ) => a
.startPeriod
- b
.startPeriod
;
809 !isArraySorted
<ChargingSchedulePeriod
>(
810 chargingSchedule
.chargingSchedulePeriod
,
811 chargingSchedulePeriodCompareFn
,
815 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
817 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
);
819 // Check if the first schedule period startPeriod property is equal to 0
820 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
822 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`,
826 // Handle only one schedule period
827 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
828 const result
: ChargingProfilesLimit
= {
829 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
832 logger
.debug(debugLogMsg
, result
);
835 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined;
836 // Search for the right schedule period
839 chargingSchedulePeriod
,
840 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
841 // Find the right schedule period
844 addSeconds(chargingSchedule
.startSchedule
!, chargingSchedulePeriod
.startPeriod
),
848 // Found the schedule period: previous is the correct one
849 const result
: ChargingProfilesLimit
= {
850 limit
: previousChargingSchedulePeriod
!.limit
,
853 logger
.debug(debugLogMsg
, result
);
856 // Keep a reference to previous one
857 previousChargingSchedulePeriod
= chargingSchedulePeriod
;
858 // Handle the last schedule period within the charging profile duration
860 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
861 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
864 chargingSchedule
.startSchedule
!,
865 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
,
867 chargingSchedule
.startSchedule
!,
868 ) > chargingSchedule
.duration
!)
870 const result
: ChargingProfilesLimit
= {
871 limit
: previousChargingSchedulePeriod
.limit
,
874 logger
.debug(debugLogMsg
, result
);
883 export const prepareChargingProfileKind
= (
884 connectorStatus
: ConnectorStatus
,
885 chargingProfile
: ChargingProfile
,
889 switch (chargingProfile
.chargingProfileKind
) {
890 case ChargingProfileKindType
.RECURRING
:
891 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
894 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
);
896 case ChargingProfileKindType
.RELATIVE
:
897 if (!isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
)) {
899 `${logPrefix} ${moduleName}.prepareChargingProfileKind: Relative charging profile id ${chargingProfile.chargingProfileId} has a startSchedule property defined. It will be ignored or used if the connector has a transaction started`,
901 delete chargingProfile
.chargingSchedule
.startSchedule
;
903 if (connectorStatus
?.transactionStarted
) {
904 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
;
906 // FIXME: Handle relative charging profile duration
912 export const canProceedChargingProfile
= (
913 chargingProfile
: ChargingProfile
,
918 (isValidTime(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
!)) ||
919 (isValidTime(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
!))
922 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
923 chargingProfile.chargingProfileId
924 } is not valid for the current date ${currentDate.toISOString()}`,
929 isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
) ||
930 isNullOrUndefined(chargingProfile
.chargingSchedule
.duration
)
933 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`,
938 !isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
) &&
939 !isValidTime(chargingProfile
.chargingSchedule
.startSchedule
)
942 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`,
947 !isNullOrUndefined(chargingProfile
.chargingSchedule
.duration
) &&
948 !Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)
951 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`,
958 const canProceedRecurringChargingProfile
= (
959 chargingProfile
: ChargingProfile
,
963 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
964 isNullOrUndefined(chargingProfile
.recurrencyKind
)
967 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
972 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
973 isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
)
976 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`,
984 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
986 * @param chargingProfile -
987 * @param currentDate -
990 const prepareRecurringChargingProfile
= (
991 chargingProfile
: ChargingProfile
,
995 const chargingSchedule
= chargingProfile
.chargingSchedule
;
996 let recurringIntervalTranslated
= false;
997 let recurringInterval
: Interval
;
998 switch (chargingProfile
.recurrencyKind
) {
999 case RecurrencyKindType
.DAILY
:
1000 recurringInterval
= {
1001 start
: chargingSchedule
.startSchedule
!,
1002 end
: addDays(chargingSchedule
.startSchedule
!, 1),
1004 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
);
1006 !isWithinInterval(currentDate
, recurringInterval
) &&
1007 isBefore(recurringInterval
.end
, currentDate
)
1009 chargingSchedule
.startSchedule
= addDays(
1010 recurringInterval
.start
,
1011 differenceInDays(currentDate
, recurringInterval
.start
),
1013 recurringInterval
= {
1014 start
: chargingSchedule
.startSchedule
,
1015 end
: addDays(chargingSchedule
.startSchedule
, 1),
1017 recurringIntervalTranslated
= true;
1020 case RecurrencyKindType
.WEEKLY
:
1021 recurringInterval
= {
1022 start
: chargingSchedule
.startSchedule
!,
1023 end
: addWeeks(chargingSchedule
.startSchedule
!, 1),
1025 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
);
1027 !isWithinInterval(currentDate
, recurringInterval
) &&
1028 isBefore(recurringInterval
.end
, currentDate
)
1030 chargingSchedule
.startSchedule
= addWeeks(
1031 recurringInterval
.start
,
1032 differenceInWeeks(currentDate
, recurringInterval
.start
),
1034 recurringInterval
= {
1035 start
: chargingSchedule
.startSchedule
,
1036 end
: addWeeks(chargingSchedule
.startSchedule
, 1),
1038 recurringIntervalTranslated
= true;
1043 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`,
1046 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1048 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1049 chargingProfile.recurrencyKind
1050 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1051 recurringInterval!.start,
1052 ).toISOString()}, ${toDate(
1053 recurringInterval!.end,
1054 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
1057 return recurringIntervalTranslated
;
1060 const checkRecurringChargingProfileDuration
= (
1061 chargingProfile
: ChargingProfile
,
1065 if (isNullOrUndefined(chargingProfile
.chargingSchedule
.duration
)) {
1067 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1068 chargingProfile.chargingProfileKind
1069 } charging profile id ${
1070 chargingProfile.chargingProfileId
1071 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1076 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
);
1078 chargingProfile
.chargingSchedule
.duration
! > differenceInSeconds(interval
.end
, interval
.start
)
1081 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1082 chargingProfile.chargingProfileKind
1083 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1084 chargingProfile.chargingSchedule.duration
1085 } is greater than the recurrency time interval duration ${differenceInSeconds(
1090 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
);
1094 const getRandomSerialNumberSuffix
= (params
?: {
1095 randomBytesLength
?: number;
1096 upperCase
?: boolean;
1098 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex');
1099 if (params
?.upperCase
) {
1100 return randomSerialNumberSuffix
.toUpperCase();
1102 return randomSerialNumberSuffix
;