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';
23 import type { ChargingStation
} from
'./ChargingStation';
24 import { getConfigurationKey
} from
'./ConfigurationKeyUtils';
25 import { BaseError
} from
'../exception';
29 type BootNotificationRequest
,
32 ChargingProfileKindType
,
34 type ChargingSchedulePeriod
,
35 type ChargingStationInfo
,
36 type ChargingStationTemplate
,
37 ChargingStationWorkerMessageEvents
,
38 ConnectorPhaseRotation
,
43 type OCPP16BootNotificationRequest
,
44 type OCPP20BootNotificationRequest
,
48 ReservationTerminationReason
,
49 StandardParametersKey
,
50 SupportedFeatureProfiles
,
72 const moduleName
= 'Helpers';
74 export const getChargingStationId
= (
76 stationTemplate
: ChargingStationTemplate
,
78 // In case of multiple instances: add instance index to charging station id
79 const instanceIndex
= process
.env
.CF_INSTANCE_INDEX
?? 0;
80 const idSuffix
= stationTemplate
?.nameSuffix
?? '';
81 const idStr
= `000000000${index.toString()}`;
82 return stationTemplate
?.fixedName
83 ? stationTemplate
.baseName
84 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
89 export const hasReservationExpired
= (reservation
: Reservation
): boolean => {
90 return isPast(reservation
.expiryDate
);
93 export const removeExpiredReservations
= async (
94 chargingStation
: ChargingStation
,
96 if (chargingStation
.hasEvses
) {
97 for (const evseStatus
of chargingStation
.evses
.values()) {
98 for (const connectorStatus
of evseStatus
.connectors
.values()) {
99 if (connectorStatus
.reservation
&& hasReservationExpired(connectorStatus
.reservation
)) {
100 await chargingStation
.removeReservation(
101 connectorStatus
.reservation
,
102 ReservationTerminationReason
.EXPIRED
,
108 for (const connectorStatus
of chargingStation
.connectors
.values()) {
109 if (connectorStatus
.reservation
&& hasReservationExpired(connectorStatus
.reservation
)) {
110 await chargingStation
.removeReservation(
111 connectorStatus
.reservation
,
112 ReservationTerminationReason
.EXPIRED
,
119 export const getNumberOfReservableConnectors
= (
120 connectors
: Map
<number, ConnectorStatus
>,
122 let numberOfReservableConnectors
= 0;
123 for (const [connectorId
, connectorStatus
] of connectors
) {
124 if (connectorId
=== 0) {
127 if (connectorStatus
.status === ConnectorStatusEnum
.Available
) {
128 ++numberOfReservableConnectors
;
131 return numberOfReservableConnectors
;
134 export const getHashId
= (index
: number, stationTemplate
: ChargingStationTemplate
): string => {
135 const chargingStationInfo
= {
136 chargePointModel
: stationTemplate
.chargePointModel
,
137 chargePointVendor
: stationTemplate
.chargePointVendor
,
138 ...(!isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
139 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
141 ...(!isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
142 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
144 ...(!isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
145 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
147 ...(!isUndefined(stationTemplate
.meterType
) && {
148 meterType
: stationTemplate
.meterType
,
151 return createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
152 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
156 export const checkChargingStation
= (
157 chargingStation
: ChargingStation
,
160 if (chargingStation
.started
=== false && chargingStation
.starting
=== false) {
161 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`);
167 export const getPhaseRotationValue
= (
169 numberOfPhases
: number,
170 ): string | undefined => {
172 if (connectorId
=== 0 && numberOfPhases
=== 0) {
173 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
174 } else if (connectorId
> 0 && numberOfPhases
=== 0) {
175 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
177 } else if (connectorId
> 0 && numberOfPhases
=== 1) {
178 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
179 } else if (connectorId
> 0 && numberOfPhases
=== 3) {
180 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
184 export const getMaxNumberOfEvses
= (evses
: Record
<string, EvseTemplate
>): number => {
188 return Object.keys(evses
).length
;
191 const getMaxNumberOfConnectors
= (connectors
: Record
<string, ConnectorStatus
>): number => {
195 return Object.keys(connectors
).length
;
198 export const getBootConnectorStatus
= (
199 chargingStation
: ChargingStation
,
201 connectorStatus
: ConnectorStatus
,
202 ): ConnectorStatusEnum
=> {
203 let connectorBootStatus
: ConnectorStatusEnum
;
205 !connectorStatus
?.status &&
206 (chargingStation
.isChargingStationAvailable() === false ||
207 chargingStation
.isConnectorAvailable(connectorId
) === false)
209 connectorBootStatus
= ConnectorStatusEnum
.Unavailable
;
210 } else if (!connectorStatus
?.status && connectorStatus
?.bootStatus
) {
211 // Set boot status in template at startup
212 connectorBootStatus
= connectorStatus
?.bootStatus
;
213 } else if (connectorStatus
?.status) {
214 // Set previous status at startup
215 connectorBootStatus
= connectorStatus
?.status;
217 // Set default status
218 connectorBootStatus
= ConnectorStatusEnum
.Available
;
220 return connectorBootStatus
;
223 export const checkTemplate
= (
224 stationTemplate
: ChargingStationTemplate
,
226 templateFile
: string,
228 if (isNullOrUndefined(stationTemplate
)) {
229 const errorMsg
= `Failed to read charging station template file ${templateFile}`;
230 logger
.error(`${logPrefix} ${errorMsg}`);
231 throw new BaseError(errorMsg
);
233 if (isEmptyObject(stationTemplate
)) {
234 const errorMsg
= `Empty charging station information from template file ${templateFile}`;
235 logger
.error(`${logPrefix} ${errorMsg}`);
236 throw new BaseError(errorMsg
);
238 if (isEmptyObject(stationTemplate
.AutomaticTransactionGenerator
!)) {
239 stationTemplate
.AutomaticTransactionGenerator
= Constants
.DEFAULT_ATG_CONFIGURATION
;
241 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
242 Constants
.DEFAULT_ATG_CONFIGURATION
,
245 if (isNullOrUndefined(stationTemplate
.idTagsFile
) || isEmptyString(stationTemplate
.idTagsFile
)) {
247 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`,
252 export const checkConnectorsConfiguration
= (
253 stationTemplate
: ChargingStationTemplate
,
255 templateFile
: string,
257 configuredMaxConnectors
: number;
258 templateMaxConnectors
: number;
259 templateMaxAvailableConnectors
: number;
261 const configuredMaxConnectors
= getConfiguredMaxNumberOfConnectors(stationTemplate
);
262 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
);
263 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
!);
264 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
);
265 const templateMaxAvailableConnectors
= stationTemplate
.Connectors
![0]
266 ? templateMaxConnectors
- 1
267 : templateMaxConnectors
;
269 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
270 !stationTemplate
?.randomConnectors
273 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`,
275 stationTemplate
.randomConnectors
= true;
277 return { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
};
280 export const checkStationInfoConnectorStatus
= (
282 connectorStatus
: ConnectorStatus
,
284 templateFile
: string,
286 if (!isNullOrUndefined(connectorStatus
?.status)) {
288 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`,
290 delete connectorStatus
.status;
294 export const buildConnectorsMap
= (
295 connectors
: Record
<string, ConnectorStatus
>,
297 templateFile
: string,
298 ): Map
<number, ConnectorStatus
> => {
299 const connectorsMap
= new Map
<number, ConnectorStatus
>();
300 if (getMaxNumberOfConnectors(connectors
) > 0) {
301 for (const connector
in connectors
) {
302 const connectorStatus
= connectors
[connector
];
303 const connectorId
= convertToInt(connector
);
304 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
);
305 connectorsMap
.set(connectorId
, cloneObject
<ConnectorStatus
>(connectorStatus
));
309 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`,
312 return connectorsMap
;
315 export const initializeConnectorsMapStatus
= (
316 connectors
: Map
<number, ConnectorStatus
>,
319 for (const connectorId
of connectors
.keys()) {
320 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
322 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
327 if (connectorId
=== 0) {
328 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
;
329 if (isUndefined(connectors
.get(connectorId
)?.chargingProfiles
)) {
330 connectors
.get(connectorId
)!.chargingProfiles
= [];
334 isNullOrUndefined(connectors
.get(connectorId
)?.transactionStarted
)
336 initializeConnectorStatus(connectors
.get(connectorId
)!);
341 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
342 connectorStatus
.chargingProfiles
=
343 connectorStatus
.transactionId
&& isNotEmptyArray(connectorStatus
.chargingProfiles
)
344 ? connectorStatus
.chargingProfiles
?.filter(
345 (chargingProfile
) => chargingProfile
.transactionId
!== connectorStatus
.transactionId
,
348 connectorStatus
.idTagLocalAuthorized
= false;
349 connectorStatus
.idTagAuthorized
= false;
350 connectorStatus
.transactionRemoteStarted
= false;
351 connectorStatus
.transactionStarted
= false;
352 delete connectorStatus
?.transactionStart
;
353 delete connectorStatus
?.transactionId
;
354 delete connectorStatus
?.localAuthorizeIdTag
;
355 delete connectorStatus
?.authorizeIdTag
;
356 delete connectorStatus
?.transactionIdTag
;
357 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0;
358 delete connectorStatus
?.transactionBeginMeterValue
;
361 export const createBootNotificationRequest
= (
362 stationInfo
: ChargingStationInfo
,
363 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
,
364 ): BootNotificationRequest
=> {
365 const ocppVersion
= stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
;
366 switch (ocppVersion
) {
367 case OCPPVersion
.VERSION_16
:
369 chargePointModel
: stationInfo
.chargePointModel
,
370 chargePointVendor
: stationInfo
.chargePointVendor
,
371 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
372 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
374 ...(!isUndefined(stationInfo
.chargePointSerialNumber
) && {
375 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
377 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
378 firmwareVersion
: stationInfo
.firmwareVersion
,
380 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
381 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
382 ...(!isUndefined(stationInfo
.meterSerialNumber
) && {
383 meterSerialNumber
: stationInfo
.meterSerialNumber
,
385 ...(!isUndefined(stationInfo
.meterType
) && {
386 meterType
: stationInfo
.meterType
,
388 } as OCPP16BootNotificationRequest
;
389 case OCPPVersion
.VERSION_20
:
390 case OCPPVersion
.VERSION_201
:
394 model
: stationInfo
.chargePointModel
,
395 vendorName
: stationInfo
.chargePointVendor
,
396 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
397 firmwareVersion
: stationInfo
.firmwareVersion
,
399 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
400 serialNumber
: stationInfo
.chargeBoxSerialNumber
,
402 ...((!isUndefined(stationInfo
.iccid
) || !isUndefined(stationInfo
.imsi
)) && {
404 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
405 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
409 } as OCPP20BootNotificationRequest
;
413 export const warnTemplateKeysDeprecation
= (
414 stationTemplate
: ChargingStationTemplate
,
416 templateFile
: string,
418 const templateKeys
: { deprecatedKey
: string; key
?: string }[] = [
419 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
420 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
421 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
422 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' },
424 for (const templateKey
of templateKeys
) {
425 warnDeprecatedTemplateKey(
427 templateKey
.deprecatedKey
,
430 !isUndefined(templateKey
.key
) ? `Use '${templateKey.key}' instead` : undefined,
432 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
);
436 export const stationTemplateToStationInfo
= (
437 stationTemplate
: ChargingStationTemplate
,
438 ): ChargingStationInfo
=> {
439 stationTemplate
= cloneObject
<ChargingStationTemplate
>(stationTemplate
);
440 delete stationTemplate
.power
;
441 delete stationTemplate
.powerUnit
;
442 delete stationTemplate
.Connectors
;
443 delete stationTemplate
.Evses
;
444 delete stationTemplate
.Configuration
;
445 delete stationTemplate
.AutomaticTransactionGenerator
;
446 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
447 delete stationTemplate
.chargePointSerialNumberPrefix
;
448 delete stationTemplate
.meterSerialNumberPrefix
;
449 return stationTemplate
as ChargingStationInfo
;
452 export const createSerialNumber
= (
453 stationTemplate
: ChargingStationTemplate
,
454 stationInfo
: ChargingStationInfo
,
456 randomSerialNumberUpperCase
?: boolean;
457 randomSerialNumber
?: boolean;
460 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
};
461 const serialNumberSuffix
= params
?.randomSerialNumber
462 ? getRandomSerialNumberSuffix({
463 upperCase
: params
.randomSerialNumberUpperCase
,
466 isNotEmptyString(stationTemplate
?.chargePointSerialNumberPrefix
) &&
467 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
468 isNotEmptyString(stationTemplate
?.chargeBoxSerialNumberPrefix
) &&
469 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
470 isNotEmptyString(stationTemplate
?.meterSerialNumberPrefix
) &&
471 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
474 export const propagateSerialNumber
= (
475 stationTemplate
: ChargingStationTemplate
,
476 stationInfoSrc
: ChargingStationInfo
,
477 stationInfoDst
: ChargingStationInfo
,
479 if (!stationInfoSrc
|| !stationTemplate
) {
481 'Missing charging station template or existing configuration to propagate serial number',
484 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
485 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
486 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
487 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
488 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
489 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
490 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
491 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
492 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
495 export const hasFeatureProfile
= (
496 chargingStation
: ChargingStation
,
497 featureProfile
: SupportedFeatureProfiles
,
498 ): boolean | undefined => {
499 return getConfigurationKey(
501 StandardParametersKey
.SupportedFeatureProfiles
,
502 )?.value
?.includes(featureProfile
);
505 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
507 switch (stationInfo
.amperageLimitationUnit
) {
508 case AmpereUnits
.DECI_AMPERE
:
511 case AmpereUnits
.CENTI_AMPERE
:
514 case AmpereUnits
.MILLI_AMPERE
:
522 * Gets the connector cloned charging profiles applying a power limitation
523 * and sorted by connector id descending then stack level descending
525 * @param chargingStation -
526 * @param connectorId -
527 * @returns connector charging profiles array
529 export const getConnectorChargingProfiles
= (
530 chargingStation
: ChargingStation
,
533 return cloneObject
<ChargingProfile
[]>(
534 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
535 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
537 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? []).sort(
538 (a
, b
) => b
.stackLevel
- a
.stackLevel
,
544 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
545 chargingStation
: ChargingStation
,
547 ): number | undefined => {
548 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined;
549 // Get charging profiles sorted by connector id then stack level
550 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
);
551 if (isNotEmptyArray(chargingProfiles
)) {
552 const result
= getLimitFromChargingProfiles(
556 chargingStation
.logPrefix(),
558 if (!isNullOrUndefined(result
)) {
559 limit
= result
?.limit
;
560 chargingProfile
= result
?.chargingProfile
;
561 switch (chargingStation
.getCurrentOutType()) {
564 chargingProfile
?.chargingSchedule
?.chargingRateUnit
=== ChargingRateUnitType
.WATT
566 : ACElectricUtils
.powerTotal(
567 chargingStation
.getNumberOfPhases(),
568 chargingStation
.getVoltageOut(),
574 chargingProfile
?.chargingSchedule
?.chargingRateUnit
=== ChargingRateUnitType
.WATT
576 : DCElectricUtils
.power(chargingStation
.getVoltageOut(), limit
!);
578 const connectorMaximumPower
=
579 chargingStation
.getMaximumPower() / chargingStation
.powerDivider
;
580 if (limit
! > connectorMaximumPower
) {
582 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${chargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
585 limit
= connectorMaximumPower
;
592 export const getDefaultVoltageOut
= (
593 currentType
: CurrentType
,
595 templateFile
: string,
597 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
598 let defaultVoltageOut
: number;
599 switch (currentType
) {
601 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
604 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
607 logger
.error(`${logPrefix} ${errorMsg}`);
608 throw new BaseError(errorMsg
);
610 return defaultVoltageOut
;
613 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
615 stationInfo
.idTagsFile
&&
616 join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
620 export const waitChargingStationEvents
= async (
621 emitter
: EventEmitter
,
622 event
: ChargingStationWorkerMessageEvents
,
623 eventsToWait
: number,
624 ): Promise
<number> => {
625 return new Promise
<number>((resolve
) => {
627 if (eventsToWait
=== 0) {
630 emitter
.on(event
, () => {
632 if (events
=== eventsToWait
) {
639 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
640 let configuredMaxNumberOfConnectors
= 0;
641 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
) === true) {
642 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
643 configuredMaxNumberOfConnectors
=
644 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)];
645 } else if (isUndefined(stationTemplate
.numberOfConnectors
) === false) {
646 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
as number;
647 } else if (stationTemplate
.Connectors
&& !stationTemplate
.Evses
) {
648 configuredMaxNumberOfConnectors
= stationTemplate
.Connectors
[0]
649 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
650 : getMaxNumberOfConnectors(stationTemplate
.Connectors
);
651 } else if (stationTemplate
.Evses
&& !stationTemplate
.Connectors
) {
652 for (const evse
in stationTemplate
.Evses
) {
656 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
657 stationTemplate
.Evses
[evse
].Connectors
,
661 return configuredMaxNumberOfConnectors
;
664 const checkConfiguredMaxConnectors
= (
665 configuredMaxConnectors
: number,
667 templateFile
: string,
669 if (configuredMaxConnectors
<= 0) {
671 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
676 const checkTemplateMaxConnectors
= (
677 templateMaxConnectors
: number,
679 templateFile
: string,
681 if (templateMaxConnectors
=== 0) {
683 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
685 } else if (templateMaxConnectors
< 0) {
687 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
692 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
693 connectorStatus
.availability
= AvailabilityType
.Operative
;
694 connectorStatus
.idTagLocalAuthorized
= false;
695 connectorStatus
.idTagAuthorized
= false;
696 connectorStatus
.transactionRemoteStarted
= false;
697 connectorStatus
.transactionStarted
= false;
698 connectorStatus
.energyActiveImportRegisterValue
= 0;
699 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0;
700 if (isUndefined(connectorStatus
.chargingProfiles
)) {
701 connectorStatus
.chargingProfiles
= [];
705 const warnDeprecatedTemplateKey
= (
706 template
: ChargingStationTemplate
,
709 templateFile
: string,
712 if (!isUndefined(template
[key
as keyof ChargingStationTemplate
])) {
713 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
714 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
716 logger
.warn(`${logPrefix} ${logMsg}`);
717 console
.warn(chalk
.yellow(`${logMsg}`));
721 const convertDeprecatedTemplateKey
= (
722 template
: ChargingStationTemplate
,
723 deprecatedKey
: string,
726 if (!isUndefined(template
[deprecatedKey
as keyof ChargingStationTemplate
])) {
727 if (!isUndefined(key
)) {
728 (template
as unknown
as Record
<string, unknown
>)[key
!] =
729 template
[deprecatedKey
as keyof ChargingStationTemplate
];
731 delete template
[deprecatedKey
as keyof ChargingStationTemplate
];
735 interface ChargingProfilesLimit
{
737 chargingProfile
: ChargingProfile
;
741 * Charging profiles shall already be sorted by connector id descending then stack level descending
743 * @param chargingStation -
744 * @param connectorId -
745 * @param chargingProfiles -
747 * @returns ChargingProfilesLimit
749 const getLimitFromChargingProfiles
= (
750 chargingStation
: ChargingStation
,
752 chargingProfiles
: ChargingProfile
[],
754 ): ChargingProfilesLimit
| undefined => {
755 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
756 const currentDate
= new Date();
757 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)!;
758 for (const chargingProfile
of chargingProfiles
) {
759 const chargingSchedule
= chargingProfile
.chargingSchedule
;
760 if (isNullOrUndefined(chargingSchedule
?.startSchedule
) && connectorStatus
?.transactionStarted
) {
762 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`,
764 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
765 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
;
768 !isNullOrUndefined(chargingSchedule
?.startSchedule
) &&
769 !isDate(chargingSchedule
?.startSchedule
)
772 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`,
774 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
?.startSchedule
)!;
777 !isNullOrUndefined(chargingSchedule
?.startSchedule
) &&
778 isNullOrUndefined(chargingSchedule
?.duration
)
781 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`,
783 // OCPP specifies that if duration is not defined, it should be infinite
784 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
!);
786 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
789 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
792 // Check if the charging profile is active
794 isWithinInterval(currentDate
, {
795 start
: chargingSchedule
.startSchedule
!,
796 end
: addSeconds(chargingSchedule
.startSchedule
!, chargingSchedule
.duration
!),
799 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
800 const chargingSchedulePeriodCompareFn
= (
801 a
: ChargingSchedulePeriod
,
802 b
: ChargingSchedulePeriod
,
803 ) => a
.startPeriod
- b
.startPeriod
;
805 !isArraySorted
<ChargingSchedulePeriod
>(
806 chargingSchedule
.chargingSchedulePeriod
,
807 chargingSchedulePeriodCompareFn
,
811 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
813 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
);
815 // Check if the first schedule period startPeriod property is equal to 0
816 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
818 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`,
822 // Handle only one schedule period
823 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
824 const result
: ChargingProfilesLimit
= {
825 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
828 logger
.debug(debugLogMsg
, result
);
831 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined;
832 // Search for the right schedule period
835 chargingSchedulePeriod
,
836 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
837 // Find the right schedule period
840 addSeconds(chargingSchedule
.startSchedule
!, chargingSchedulePeriod
.startPeriod
),
844 // Found the schedule period: previous is the correct one
845 const result
: ChargingProfilesLimit
= {
846 limit
: previousChargingSchedulePeriod
!.limit
,
849 logger
.debug(debugLogMsg
, result
);
852 // Keep a reference to previous one
853 previousChargingSchedulePeriod
= chargingSchedulePeriod
;
854 // Handle the last schedule period within the charging profile duration
856 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
857 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
860 chargingSchedule
.startSchedule
!,
861 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
,
863 chargingSchedule
.startSchedule
!,
864 ) > chargingSchedule
.duration
!)
866 const result
: ChargingProfilesLimit
= {
867 limit
: previousChargingSchedulePeriod
.limit
,
870 logger
.debug(debugLogMsg
, result
);
879 export const prepareChargingProfileKind
= (
880 connectorStatus
: ConnectorStatus
,
881 chargingProfile
: ChargingProfile
,
885 switch (chargingProfile
.chargingProfileKind
) {
886 case ChargingProfileKindType
.RECURRING
:
887 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
890 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
);
892 case ChargingProfileKindType
.RELATIVE
:
893 if (!isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
)) {
895 `${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`,
897 delete chargingProfile
.chargingSchedule
.startSchedule
;
899 if (connectorStatus
?.transactionStarted
) {
900 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
;
902 // FIXME: Handle relative charging profile duration
908 export const canProceedChargingProfile
= (
909 chargingProfile
: ChargingProfile
,
914 (isValidTime(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
!)) ||
915 (isValidTime(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
!))
918 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
919 chargingProfile.chargingProfileId
920 } is not valid for the current date ${currentDate.toISOString()}`,
925 isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
) ||
926 isNullOrUndefined(chargingProfile
.chargingSchedule
.duration
)
929 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`,
934 !isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
) &&
935 !isValidTime(chargingProfile
.chargingSchedule
.startSchedule
)
938 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`,
943 !isNullOrUndefined(chargingProfile
.chargingSchedule
.duration
) &&
944 !Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)
947 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`,
954 const canProceedRecurringChargingProfile
= (
955 chargingProfile
: ChargingProfile
,
959 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
960 isNullOrUndefined(chargingProfile
.recurrencyKind
)
963 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
968 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
969 isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
)
972 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`,
980 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
982 * @param chargingProfile -
983 * @param currentDate -
986 const prepareRecurringChargingProfile
= (
987 chargingProfile
: ChargingProfile
,
991 const chargingSchedule
= chargingProfile
.chargingSchedule
;
992 let recurringIntervalTranslated
= false;
993 let recurringInterval
: Interval
;
994 switch (chargingProfile
.recurrencyKind
) {
995 case RecurrencyKindType
.DAILY
:
996 recurringInterval
= {
997 start
: chargingSchedule
.startSchedule
!,
998 end
: addDays(chargingSchedule
.startSchedule
!, 1),
1000 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
);
1002 !isWithinInterval(currentDate
, recurringInterval
) &&
1003 isBefore(recurringInterval
.end
, currentDate
)
1005 chargingSchedule
.startSchedule
= addDays(
1006 recurringInterval
.start
,
1007 differenceInDays(currentDate
, recurringInterval
.start
),
1009 recurringInterval
= {
1010 start
: chargingSchedule
.startSchedule
,
1011 end
: addDays(chargingSchedule
.startSchedule
, 1),
1013 recurringIntervalTranslated
= true;
1016 case RecurrencyKindType
.WEEKLY
:
1017 recurringInterval
= {
1018 start
: chargingSchedule
.startSchedule
!,
1019 end
: addWeeks(chargingSchedule
.startSchedule
!, 1),
1021 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
);
1023 !isWithinInterval(currentDate
, recurringInterval
) &&
1024 isBefore(recurringInterval
.end
, currentDate
)
1026 chargingSchedule
.startSchedule
= addWeeks(
1027 recurringInterval
.start
,
1028 differenceInWeeks(currentDate
, recurringInterval
.start
),
1030 recurringInterval
= {
1031 start
: chargingSchedule
.startSchedule
,
1032 end
: addWeeks(chargingSchedule
.startSchedule
, 1),
1034 recurringIntervalTranslated
= true;
1039 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`,
1042 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1044 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1045 chargingProfile.recurrencyKind
1046 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1047 recurringInterval!.start,
1048 ).toISOString()}, ${toDate(
1049 recurringInterval!.end,
1050 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
1053 return recurringIntervalTranslated
;
1056 const checkRecurringChargingProfileDuration
= (
1057 chargingProfile
: ChargingProfile
,
1061 if (isNullOrUndefined(chargingProfile
.chargingSchedule
.duration
)) {
1063 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1064 chargingProfile.chargingProfileKind
1065 } charging profile id ${
1066 chargingProfile.chargingProfileId
1067 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1072 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
);
1074 chargingProfile
.chargingSchedule
.duration
! > differenceInSeconds(interval
.end
, interval
.start
)
1077 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1078 chargingProfile.chargingProfileKind
1079 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1080 chargingProfile.chargingSchedule.duration
1081 } is greater than the recurrency time interval duration ${differenceInSeconds(
1086 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
);
1090 const getRandomSerialNumberSuffix
= (params
?: {
1091 randomBytesLength
?: number;
1092 upperCase
?: boolean;
1094 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex');
1095 if (params
?.upperCase
) {
1096 return randomSerialNumberSuffix
.toUpperCase();
1098 return randomSerialNumberSuffix
;