1 import { createHash
, randomBytes
} from
'node:crypto';
2 import type { EventEmitter
} from
'node:events';
3 import { basename
, dirname
, join
} from
'node:path';
4 import { fileURLToPath
} from
'node:url';
6 import chalk from
'chalk';
22 import type { ChargingStation
} from
'./ChargingStation';
23 import { getConfigurationKey
} from
'./ConfigurationKeyUtils';
24 import { BaseError
} from
'../exception';
28 type BootNotificationRequest
,
31 ChargingProfileKindType
,
33 type ChargingSchedulePeriod
,
34 type ChargingStationInfo
,
35 type ChargingStationTemplate
,
36 ChargingStationWorkerMessageEvents
,
37 ConnectorPhaseRotation
,
42 type OCPP16BootNotificationRequest
,
43 type OCPP20BootNotificationRequest
,
47 ReservationTerminationReason
,
48 StandardParametersKey
,
49 SupportedFeatureProfiles
,
71 const moduleName
= 'Helpers';
73 export const getChargingStationId
= (
75 stationTemplate
: ChargingStationTemplate
,
77 // In case of multiple instances: add instance index to charging station id
78 const instanceIndex
= process
.env
.CF_INSTANCE_INDEX
?? 0;
79 const idSuffix
= stationTemplate
?.nameSuffix
?? '';
80 const idStr
= `000000000${index.toString()}`;
81 return stationTemplate
?.fixedName
82 ? stationTemplate
.baseName
83 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
88 export const hasReservationExpired
= (reservation
: Reservation
): boolean => {
89 return isPast(reservation
.expiryDate
);
92 export const removeExpiredReservations
= async (
93 chargingStation
: ChargingStation
,
95 if (chargingStation
.hasEvses
) {
96 for (const evseStatus
of chargingStation
.evses
.values()) {
97 for (const connectorStatus
of evseStatus
.connectors
.values()) {
98 if (connectorStatus
.reservation
&& hasReservationExpired(connectorStatus
.reservation
)) {
99 await chargingStation
.removeReservation(
100 connectorStatus
.reservation
,
101 ReservationTerminationReason
.EXPIRED
,
107 for (const connectorStatus
of chargingStation
.connectors
.values()) {
108 if (connectorStatus
.reservation
&& hasReservationExpired(connectorStatus
.reservation
)) {
109 await chargingStation
.removeReservation(
110 connectorStatus
.reservation
,
111 ReservationTerminationReason
.EXPIRED
,
118 export const getNumberOfReservableConnectors
= (
119 connectors
: Map
<number, ConnectorStatus
>,
121 let numberOfReservableConnectors
= 0;
122 for (const [connectorId
, connectorStatus
] of connectors
) {
123 if (connectorId
=== 0) {
126 if (connectorStatus
.status === ConnectorStatusEnum
.Available
) {
127 ++numberOfReservableConnectors
;
130 return numberOfReservableConnectors
;
133 export const getHashId
= (index
: number, stationTemplate
: ChargingStationTemplate
): string => {
134 const chargingStationInfo
= {
135 chargePointModel
: stationTemplate
.chargePointModel
,
136 chargePointVendor
: stationTemplate
.chargePointVendor
,
137 ...(!isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
138 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
140 ...(!isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
141 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
143 ...(!isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
144 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
146 ...(!isUndefined(stationTemplate
.meterType
) && {
147 meterType
: stationTemplate
.meterType
,
150 return createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
151 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
155 export const checkChargingStation
= (
156 chargingStation
: ChargingStation
,
159 if (chargingStation
.started
=== false && chargingStation
.starting
=== false) {
160 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`);
166 export const getPhaseRotationValue
= (
168 numberOfPhases
: number,
169 ): string | undefined => {
171 if (connectorId
=== 0 && numberOfPhases
=== 0) {
172 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
173 } else if (connectorId
> 0 && numberOfPhases
=== 0) {
174 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
176 } else if (connectorId
> 0 && numberOfPhases
=== 1) {
177 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
178 } else if (connectorId
> 0 && numberOfPhases
=== 3) {
179 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
183 export const getMaxNumberOfEvses
= (evses
: Record
<string, EvseTemplate
>): number => {
187 return Object.keys(evses
).length
;
190 const getMaxNumberOfConnectors
= (connectors
: Record
<string, ConnectorStatus
>): number => {
194 return Object.keys(connectors
).length
;
197 export const getBootConnectorStatus
= (
198 chargingStation
: ChargingStation
,
200 connectorStatus
: ConnectorStatus
,
201 ): ConnectorStatusEnum
=> {
202 let connectorBootStatus
: ConnectorStatusEnum
;
204 !connectorStatus
?.status &&
205 (chargingStation
.isChargingStationAvailable() === false ||
206 chargingStation
.isConnectorAvailable(connectorId
) === false)
208 connectorBootStatus
= ConnectorStatusEnum
.Unavailable
;
209 } else if (!connectorStatus
?.status && connectorStatus
?.bootStatus
) {
210 // Set boot status in template at startup
211 connectorBootStatus
= connectorStatus
?.bootStatus
;
212 } else if (connectorStatus
?.status) {
213 // Set previous status at startup
214 connectorBootStatus
= connectorStatus
?.status;
216 // Set default status
217 connectorBootStatus
= ConnectorStatusEnum
.Available
;
219 return connectorBootStatus
;
222 export const checkTemplate
= (
223 stationTemplate
: ChargingStationTemplate
,
225 templateFile
: string,
227 if (isNullOrUndefined(stationTemplate
)) {
228 const errorMsg
= `Failed to read charging station template file ${templateFile}`;
229 logger
.error(`${logPrefix} ${errorMsg}`);
230 throw new BaseError(errorMsg
);
232 if (isEmptyObject(stationTemplate
)) {
233 const errorMsg
= `Empty charging station information from template file ${templateFile}`;
234 logger
.error(`${logPrefix} ${errorMsg}`);
235 throw new BaseError(errorMsg
);
237 if (isEmptyObject(stationTemplate
.AutomaticTransactionGenerator
!)) {
238 stationTemplate
.AutomaticTransactionGenerator
= Constants
.DEFAULT_ATG_CONFIGURATION
;
240 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
241 Constants
.DEFAULT_ATG_CONFIGURATION
,
244 if (isNullOrUndefined(stationTemplate
.idTagsFile
) || isEmptyString(stationTemplate
.idTagsFile
)) {
246 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`,
251 export const checkConnectorsConfiguration
= (
252 stationTemplate
: ChargingStationTemplate
,
254 templateFile
: string,
256 configuredMaxConnectors
: number;
257 templateMaxConnectors
: number;
258 templateMaxAvailableConnectors
: number;
260 const configuredMaxConnectors
= getConfiguredMaxNumberOfConnectors(stationTemplate
);
261 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
);
262 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
!);
263 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
);
264 const templateMaxAvailableConnectors
= stationTemplate
.Connectors
![0]
265 ? templateMaxConnectors
- 1
266 : templateMaxConnectors
;
268 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
269 !stationTemplate
?.randomConnectors
272 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`,
274 stationTemplate
.randomConnectors
= true;
276 return { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
};
279 export const checkStationInfoConnectorStatus
= (
281 connectorStatus
: ConnectorStatus
,
283 templateFile
: string,
285 if (!isNullOrUndefined(connectorStatus
?.status)) {
287 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`,
289 delete connectorStatus
.status;
293 export const buildConnectorsMap
= (
294 connectors
: Record
<string, ConnectorStatus
>,
296 templateFile
: string,
297 ): Map
<number, ConnectorStatus
> => {
298 const connectorsMap
= new Map
<number, ConnectorStatus
>();
299 if (getMaxNumberOfConnectors(connectors
) > 0) {
300 for (const connector
in connectors
) {
301 const connectorStatus
= connectors
[connector
];
302 const connectorId
= convertToInt(connector
);
303 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
);
304 connectorsMap
.set(connectorId
, cloneObject
<ConnectorStatus
>(connectorStatus
));
308 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`,
311 return connectorsMap
;
314 export const initializeConnectorsMapStatus
= (
315 connectors
: Map
<number, ConnectorStatus
>,
318 for (const connectorId
of connectors
.keys()) {
319 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
321 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
326 if (connectorId
=== 0) {
327 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
;
328 if (isUndefined(connectors
.get(connectorId
)?.chargingProfiles
)) {
329 connectors
.get(connectorId
)!.chargingProfiles
= [];
333 isNullOrUndefined(connectors
.get(connectorId
)?.transactionStarted
)
335 initializeConnectorStatus(connectors
.get(connectorId
)!);
340 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
341 connectorStatus
.idTagLocalAuthorized
= false;
342 connectorStatus
.idTagAuthorized
= false;
343 connectorStatus
.transactionRemoteStarted
= false;
344 connectorStatus
.transactionStarted
= false;
345 delete connectorStatus
?.transactionStart
;
346 delete connectorStatus
?.transactionId
;
347 delete connectorStatus
?.localAuthorizeIdTag
;
348 delete connectorStatus
?.authorizeIdTag
;
349 delete connectorStatus
?.transactionIdTag
;
350 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0;
351 delete connectorStatus
?.transactionBeginMeterValue
;
354 export const createBootNotificationRequest
= (
355 stationInfo
: ChargingStationInfo
,
356 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
,
357 ): BootNotificationRequest
=> {
358 const ocppVersion
= stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
;
359 switch (ocppVersion
) {
360 case OCPPVersion
.VERSION_16
:
362 chargePointModel
: stationInfo
.chargePointModel
,
363 chargePointVendor
: stationInfo
.chargePointVendor
,
364 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
365 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
367 ...(!isUndefined(stationInfo
.chargePointSerialNumber
) && {
368 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
370 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
371 firmwareVersion
: stationInfo
.firmwareVersion
,
373 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
374 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
375 ...(!isUndefined(stationInfo
.meterSerialNumber
) && {
376 meterSerialNumber
: stationInfo
.meterSerialNumber
,
378 ...(!isUndefined(stationInfo
.meterType
) && {
379 meterType
: stationInfo
.meterType
,
381 } as OCPP16BootNotificationRequest
;
382 case OCPPVersion
.VERSION_20
:
383 case OCPPVersion
.VERSION_201
:
387 model
: stationInfo
.chargePointModel
,
388 vendorName
: stationInfo
.chargePointVendor
,
389 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
390 firmwareVersion
: stationInfo
.firmwareVersion
,
392 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
393 serialNumber
: stationInfo
.chargeBoxSerialNumber
,
395 ...((!isUndefined(stationInfo
.iccid
) || !isUndefined(stationInfo
.imsi
)) && {
397 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
398 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
402 } as OCPP20BootNotificationRequest
;
406 export const warnTemplateKeysDeprecation
= (
407 stationTemplate
: ChargingStationTemplate
,
409 templateFile
: string,
411 const templateKeys
: { deprecatedKey
: string; key
?: string }[] = [
412 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
413 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
414 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
415 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' },
417 for (const templateKey
of templateKeys
) {
418 warnDeprecatedTemplateKey(
420 templateKey
.deprecatedKey
,
423 !isUndefined(templateKey
.key
) ? `Use '${templateKey.key}' instead` : undefined,
425 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
);
429 export const stationTemplateToStationInfo
= (
430 stationTemplate
: ChargingStationTemplate
,
431 ): ChargingStationInfo
=> {
432 stationTemplate
= cloneObject
<ChargingStationTemplate
>(stationTemplate
);
433 delete stationTemplate
.power
;
434 delete stationTemplate
.powerUnit
;
435 delete stationTemplate
.Connectors
;
436 delete stationTemplate
.Evses
;
437 delete stationTemplate
.Configuration
;
438 delete stationTemplate
.AutomaticTransactionGenerator
;
439 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
440 delete stationTemplate
.chargePointSerialNumberPrefix
;
441 delete stationTemplate
.meterSerialNumberPrefix
;
442 return stationTemplate
as ChargingStationInfo
;
445 export const createSerialNumber
= (
446 stationTemplate
: ChargingStationTemplate
,
447 stationInfo
: ChargingStationInfo
,
449 randomSerialNumberUpperCase
?: boolean;
450 randomSerialNumber
?: boolean;
452 randomSerialNumberUpperCase
: true,
453 randomSerialNumber
: true,
456 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
};
457 const serialNumberSuffix
= params
?.randomSerialNumber
458 ? getRandomSerialNumberSuffix({
459 upperCase
: params
.randomSerialNumberUpperCase
,
462 isNotEmptyString(stationTemplate
?.chargePointSerialNumberPrefix
) &&
463 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
464 isNotEmptyString(stationTemplate
?.chargeBoxSerialNumberPrefix
) &&
465 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
466 isNotEmptyString(stationTemplate
?.meterSerialNumberPrefix
) &&
467 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
470 export const propagateSerialNumber
= (
471 stationTemplate
: ChargingStationTemplate
,
472 stationInfoSrc
: ChargingStationInfo
,
473 stationInfoDst
: ChargingStationInfo
,
475 if (!stationInfoSrc
|| !stationTemplate
) {
477 'Missing charging station template or existing configuration to propagate serial number',
480 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
481 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
482 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
483 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
484 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
485 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
486 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
487 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
488 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
491 export const hasFeatureProfile
= (
492 chargingStation
: ChargingStation
,
493 featureProfile
: SupportedFeatureProfiles
,
494 ): boolean | undefined => {
495 return getConfigurationKey(
497 StandardParametersKey
.SupportedFeatureProfiles
,
498 )?.value
?.includes(featureProfile
);
501 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
503 switch (stationInfo
.amperageLimitationUnit
) {
504 case AmpereUnits
.DECI_AMPERE
:
507 case AmpereUnits
.CENTI_AMPERE
:
510 case AmpereUnits
.MILLI_AMPERE
:
518 * Gets the connector cloned charging profiles applying a power limitation
519 * and sorted by connector id ascending then stack level descending
521 * @param chargingStation -
522 * @param connectorId -
523 * @returns connector charging profiles array
525 export const getConnectorChargingProfiles
= (
526 chargingStation
: ChargingStation
,
529 return cloneObject
<ChargingProfile
[]>(
530 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? [])
531 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
533 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? []).sort(
534 (a
, b
) => b
.stackLevel
- a
.stackLevel
,
540 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
541 chargingStation
: ChargingStation
,
543 ): number | undefined => {
544 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined;
545 // Get charging profiles sorted by connector id then stack level
546 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
);
547 if (isNotEmptyArray(chargingProfiles
)) {
548 const result
= getLimitFromChargingProfiles(
552 chargingStation
.logPrefix(),
554 if (!isNullOrUndefined(result
)) {
555 limit
= result
?.limit
;
556 chargingProfile
= result
?.chargingProfile
;
557 switch (chargingStation
.getCurrentOutType()) {
560 chargingProfile
?.chargingSchedule
?.chargingRateUnit
=== ChargingRateUnitType
.WATT
562 : ACElectricUtils
.powerTotal(
563 chargingStation
.getNumberOfPhases(),
564 chargingStation
.getVoltageOut(),
570 chargingProfile
?.chargingSchedule
?.chargingRateUnit
=== ChargingRateUnitType
.WATT
572 : DCElectricUtils
.power(chargingStation
.getVoltageOut(), limit
!);
574 const connectorMaximumPower
=
575 chargingStation
.getMaximumPower() / chargingStation
.powerDivider
;
576 if (limit
! > connectorMaximumPower
) {
578 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${chargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
581 limit
= connectorMaximumPower
;
588 export const getDefaultVoltageOut
= (
589 currentType
: CurrentType
,
591 templateFile
: string,
593 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
594 let defaultVoltageOut
: number;
595 switch (currentType
) {
597 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
600 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
603 logger
.error(`${logPrefix} ${errorMsg}`);
604 throw new BaseError(errorMsg
);
606 return defaultVoltageOut
;
609 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
611 stationInfo
.idTagsFile
&&
612 join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
616 export const waitChargingStationEvents
= async (
617 emitter
: EventEmitter
,
618 event
: ChargingStationWorkerMessageEvents
,
619 eventsToWait
: number,
620 ): Promise
<number> => {
621 return new Promise
<number>((resolve
) => {
623 if (eventsToWait
=== 0) {
626 emitter
.on(event
, () => {
628 if (events
=== eventsToWait
) {
635 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
636 let configuredMaxNumberOfConnectors
= 0;
637 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
) === true) {
638 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
639 configuredMaxNumberOfConnectors
=
640 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)];
641 } else if (isUndefined(stationTemplate
.numberOfConnectors
) === false) {
642 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
as number;
643 } else if (stationTemplate
.Connectors
&& !stationTemplate
.Evses
) {
644 configuredMaxNumberOfConnectors
= stationTemplate
.Connectors
[0]
645 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
646 : getMaxNumberOfConnectors(stationTemplate
.Connectors
);
647 } else if (stationTemplate
.Evses
&& !stationTemplate
.Connectors
) {
648 for (const evse
in stationTemplate
.Evses
) {
652 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
653 stationTemplate
.Evses
[evse
].Connectors
,
657 return configuredMaxNumberOfConnectors
;
660 const checkConfiguredMaxConnectors
= (
661 configuredMaxConnectors
: number,
663 templateFile
: string,
665 if (configuredMaxConnectors
<= 0) {
667 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
672 const checkTemplateMaxConnectors
= (
673 templateMaxConnectors
: number,
675 templateFile
: string,
677 if (templateMaxConnectors
=== 0) {
679 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
681 } else if (templateMaxConnectors
< 0) {
683 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
688 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
689 connectorStatus
.availability
= AvailabilityType
.Operative
;
690 connectorStatus
.idTagLocalAuthorized
= false;
691 connectorStatus
.idTagAuthorized
= false;
692 connectorStatus
.transactionRemoteStarted
= false;
693 connectorStatus
.transactionStarted
= false;
694 connectorStatus
.energyActiveImportRegisterValue
= 0;
695 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0;
696 if (isUndefined(connectorStatus
.chargingProfiles
)) {
697 connectorStatus
.chargingProfiles
= [];
701 const warnDeprecatedTemplateKey
= (
702 template
: ChargingStationTemplate
,
705 templateFile
: string,
708 if (!isUndefined(template
[key
as keyof ChargingStationTemplate
])) {
709 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
710 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
712 logger
.warn(`${logPrefix} ${logMsg}`);
713 console
.warn(chalk
.yellow(`${logMsg}`));
717 const convertDeprecatedTemplateKey
= (
718 template
: ChargingStationTemplate
,
719 deprecatedKey
: string,
722 if (!isUndefined(template
[deprecatedKey
as keyof ChargingStationTemplate
])) {
723 if (!isUndefined(key
)) {
724 (template
as unknown
as Record
<string, unknown
>)[key
!] =
725 template
[deprecatedKey
as keyof ChargingStationTemplate
];
727 delete template
[deprecatedKey
as keyof ChargingStationTemplate
];
731 interface ChargingProfilesLimit
{
733 chargingProfile
: ChargingProfile
;
737 * Charging profiles shall already be sorted by connector id ascending then stack level descending
739 * @param chargingStation -
740 * @param connectorId -
741 * @param chargingProfiles -
743 * @returns ChargingProfilesLimit
745 const getLimitFromChargingProfiles
= (
746 chargingStation
: ChargingStation
,
748 chargingProfiles
: ChargingProfile
[],
750 ): ChargingProfilesLimit
| undefined => {
751 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
752 const currentDate
= new Date();
753 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)!;
754 for (const chargingProfile
of chargingProfiles
) {
755 const chargingSchedule
= chargingProfile
.chargingSchedule
;
756 if (connectorStatus
?.transactionStarted
&& isNullOrUndefined(chargingSchedule
?.startSchedule
)) {
758 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`,
760 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
761 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
;
763 if (!isDate(chargingSchedule
?.startSchedule
)) {
765 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date object. Trying to convert it to a Date object`,
767 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
?.startSchedule
)!;
769 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
772 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
775 // Check if the charging profile is active
777 isValidTime(chargingSchedule
?.startSchedule
) &&
778 isWithinInterval(currentDate
, {
779 start
: chargingSchedule
.startSchedule
!,
780 end
: addSeconds(chargingSchedule
.startSchedule
!, chargingSchedule
.duration
!),
783 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
784 const chargingSchedulePeriodCompareFn
= (
785 a
: ChargingSchedulePeriod
,
786 b
: ChargingSchedulePeriod
,
787 ) => a
.startPeriod
- b
.startPeriod
;
789 !isArraySorted
<ChargingSchedulePeriod
>(
790 chargingSchedule
.chargingSchedulePeriod
,
791 chargingSchedulePeriodCompareFn
,
795 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
797 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
);
799 // Check if the first schedule period start period is equal to 0
800 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
802 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`,
806 // Handle only one schedule period
807 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
808 const result
: ChargingProfilesLimit
= {
809 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
812 logger
.debug(debugLogMsg
, result
);
815 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined;
816 // Search for the right schedule period
819 chargingSchedulePeriod
,
820 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
821 // Find the right schedule period
824 addSeconds(chargingSchedule
.startSchedule
!, chargingSchedulePeriod
.startPeriod
),
828 // Found the schedule period: previous is the correct one
829 const result
: ChargingProfilesLimit
= {
830 limit
: previousChargingSchedulePeriod
!.limit
,
833 logger
.debug(debugLogMsg
, result
);
836 // Keep a reference to previous one
837 previousChargingSchedulePeriod
= chargingSchedulePeriod
;
838 // Handle the last schedule period within the charging profile duration
840 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
841 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
844 chargingSchedule
.startSchedule
!,
845 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
,
847 chargingSchedule
.startSchedule
!,
848 ) > chargingSchedule
.duration
!)
850 const result
: ChargingProfilesLimit
= {
851 limit
: previousChargingSchedulePeriod
.limit
,
854 logger
.debug(debugLogMsg
, result
);
863 export const prepareChargingProfileKind
= (
864 connectorStatus
: ConnectorStatus
,
865 chargingProfile
: ChargingProfile
,
869 switch (chargingProfile
.chargingProfileKind
) {
870 case ChargingProfileKindType
.RECURRING
:
871 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
874 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
);
876 case ChargingProfileKindType
.RELATIVE
:
877 if (!isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
)) {
879 `${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`,
881 delete chargingProfile
.chargingSchedule
.startSchedule
;
883 connectorStatus
?.transactionStarted
&&
884 (chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
);
890 export const canProceedChargingProfile
= (
891 chargingProfile
: ChargingProfile
,
896 (isValidTime(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
!)) ||
897 (isValidTime(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
!))
900 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
901 chargingProfile.chargingProfileId
902 } is not valid for the current date ${currentDate.toISOString()}`,
906 const chargingSchedule
= chargingProfile
.chargingSchedule
;
907 if (isNullOrUndefined(chargingSchedule
?.startSchedule
)) {
909 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`,
913 if (isNullOrUndefined(chargingSchedule
?.duration
)) {
915 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined, not yet supported`,
922 const canProceedRecurringChargingProfile
= (
923 chargingProfile
: ChargingProfile
,
927 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
928 isNullOrUndefined(chargingProfile
.recurrencyKind
)
931 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
939 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
941 * @param chargingProfile -
942 * @param currentDate -
945 const prepareRecurringChargingProfile
= (
946 chargingProfile
: ChargingProfile
,
950 const chargingSchedule
= chargingProfile
.chargingSchedule
;
951 let recurringIntervalTranslated
= false;
952 let recurringInterval
: Interval
;
953 switch (chargingProfile
.recurrencyKind
) {
954 case RecurrencyKindType
.DAILY
:
955 recurringInterval
= {
956 start
: chargingSchedule
.startSchedule
!,
957 end
: addDays(chargingSchedule
.startSchedule
!, 1),
959 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
);
961 !isWithinInterval(currentDate
, recurringInterval
) &&
962 isBefore(recurringInterval
.end
, currentDate
)
964 chargingSchedule
.startSchedule
= addDays(
965 recurringInterval
.start
,
966 differenceInDays(currentDate
, recurringInterval
.start
),
968 recurringInterval
= {
969 start
: chargingSchedule
.startSchedule
,
970 end
: addDays(chargingSchedule
.startSchedule
, 1),
972 recurringIntervalTranslated
= true;
975 case RecurrencyKindType
.WEEKLY
:
976 recurringInterval
= {
977 start
: chargingSchedule
.startSchedule
!,
978 end
: addWeeks(chargingSchedule
.startSchedule
!, 1),
980 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
);
982 !isWithinInterval(currentDate
, recurringInterval
) &&
983 isBefore(recurringInterval
.end
, currentDate
)
985 chargingSchedule
.startSchedule
= addWeeks(
986 recurringInterval
.start
,
987 differenceInWeeks(currentDate
, recurringInterval
.start
),
989 recurringInterval
= {
990 start
: chargingSchedule
.startSchedule
,
991 end
: addWeeks(chargingSchedule
.startSchedule
, 1),
993 recurringIntervalTranslated
= true;
998 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`,
1001 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1003 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1004 chargingProfile.recurrencyKind
1005 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1006 recurringInterval!.start,
1007 ).toISOString()}, ${toDate(
1008 recurringInterval!.end,
1009 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
1012 return recurringIntervalTranslated
;
1015 const checkRecurringChargingProfileDuration
= (
1016 chargingProfile
: ChargingProfile
,
1020 if (isNullOrUndefined(chargingProfile
.chargingSchedule
.duration
)) {
1022 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1023 chargingProfile.chargingProfileKind
1024 } charging profile id ${
1025 chargingProfile.chargingProfileId
1026 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1031 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
);
1033 chargingProfile
.chargingSchedule
.duration
! > differenceInSeconds(interval
.end
, interval
.start
)
1036 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1037 chargingProfile.chargingProfileKind
1038 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1039 chargingProfile.chargingSchedule.duration
1040 } is greater than the recurrency time interval duration ${differenceInSeconds(
1045 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
);
1049 const getRandomSerialNumberSuffix
= (params
?: {
1050 randomBytesLength
?: number;
1051 upperCase
?: boolean;
1053 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex');
1054 if (params
?.upperCase
) {
1055 return randomSerialNumberSuffix
.toUpperCase();
1057 return randomSerialNumberSuffix
;