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 ChargingStationConfiguration
,
37 type ChargingStationInfo
,
38 type ChargingStationTemplate
,
39 ChargingStationWorkerMessageEvents
,
40 ConnectorPhaseRotation
,
45 type OCPP16BootNotificationRequest
,
46 type OCPP20BootNotificationRequest
,
50 ReservationTerminationReason
,
51 StandardParametersKey
,
52 SupportedFeatureProfiles
,
74 const moduleName
= 'Helpers';
76 export const getChargingStationId
= (
78 stationTemplate
: ChargingStationTemplate
| undefined,
80 if (stationTemplate
=== undefined) {
81 return "Unknown 'chargingStationId'";
83 // In case of multiple instances: add instance index to charging station id
84 const instanceIndex
= env
.CF_INSTANCE_INDEX
?? 0;
85 const idSuffix
= stationTemplate
?.nameSuffix
?? '';
86 const idStr
= `000000000${index.toString()}`;
87 return stationTemplate
?.fixedName
88 ? stationTemplate
.baseName
89 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
94 export const hasReservationExpired
= (reservation
: Reservation
): boolean => {
95 return isPast(reservation
.expiryDate
);
98 export const removeExpiredReservations
= async (
99 chargingStation
: ChargingStation
,
100 ): Promise
<void> => {
101 if (chargingStation
.hasEvses
) {
102 for (const evseStatus
of chargingStation
.evses
.values()) {
103 for (const connectorStatus
of evseStatus
.connectors
.values()) {
104 if (connectorStatus
.reservation
&& hasReservationExpired(connectorStatus
.reservation
)) {
105 await chargingStation
.removeReservation(
106 connectorStatus
.reservation
,
107 ReservationTerminationReason
.EXPIRED
,
113 for (const connectorStatus
of chargingStation
.connectors
.values()) {
114 if (connectorStatus
.reservation
&& hasReservationExpired(connectorStatus
.reservation
)) {
115 await chargingStation
.removeReservation(
116 connectorStatus
.reservation
,
117 ReservationTerminationReason
.EXPIRED
,
124 export const getNumberOfReservableConnectors
= (
125 connectors
: Map
<number, ConnectorStatus
>,
127 let numberOfReservableConnectors
= 0;
128 for (const [connectorId
, connectorStatus
] of connectors
) {
129 if (connectorId
=== 0) {
132 if (connectorStatus
.status === ConnectorStatusEnum
.Available
) {
133 ++numberOfReservableConnectors
;
136 return numberOfReservableConnectors
;
139 export const getHashId
= (index
: number, stationTemplate
: ChargingStationTemplate
): string => {
140 const chargingStationInfo
= {
141 chargePointModel
: stationTemplate
.chargePointModel
,
142 chargePointVendor
: stationTemplate
.chargePointVendor
,
143 ...(!isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
144 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
146 ...(!isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
147 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
149 ...(!isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
150 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
152 ...(!isUndefined(stationTemplate
.meterType
) && {
153 meterType
: stationTemplate
.meterType
,
156 return createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
157 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
161 export const checkChargingStation
= (
162 chargingStation
: ChargingStation
,
165 if (chargingStation
.started
=== false && chargingStation
.starting
=== false) {
166 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`);
172 export const getPhaseRotationValue
= (
174 numberOfPhases
: number,
175 ): string | undefined => {
177 if (connectorId
=== 0 && numberOfPhases
=== 0) {
178 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
179 } else if (connectorId
> 0 && numberOfPhases
=== 0) {
180 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
182 } else if (connectorId
>= 0 && numberOfPhases
=== 1) {
183 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
184 } else if (connectorId
>= 0 && numberOfPhases
=== 3) {
185 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
189 export const getMaxNumberOfEvses
= (evses
: Record
<string, EvseTemplate
>): number => {
193 return Object.keys(evses
).length
;
196 const getMaxNumberOfConnectors
= (connectors
: Record
<string, ConnectorStatus
>): number => {
200 return Object.keys(connectors
).length
;
203 export const getBootConnectorStatus
= (
204 chargingStation
: ChargingStation
,
206 connectorStatus
: ConnectorStatus
,
207 ): ConnectorStatusEnum
=> {
208 let connectorBootStatus
: ConnectorStatusEnum
;
210 !connectorStatus
?.status &&
211 (chargingStation
.isChargingStationAvailable() === false ||
212 chargingStation
.isConnectorAvailable(connectorId
) === false)
214 connectorBootStatus
= ConnectorStatusEnum
.Unavailable
;
215 } else if (!connectorStatus
?.status && connectorStatus
?.bootStatus
) {
216 // Set boot status in template at startup
217 connectorBootStatus
= connectorStatus
?.bootStatus
;
218 } else if (connectorStatus
?.status) {
219 // Set previous status at startup
220 connectorBootStatus
= connectorStatus
?.status;
222 // Set default status
223 connectorBootStatus
= ConnectorStatusEnum
.Available
;
225 return connectorBootStatus
;
228 export const checkTemplate
= (
229 stationTemplate
: ChargingStationTemplate
,
231 templateFile
: string,
233 if (isNullOrUndefined(stationTemplate
)) {
234 const errorMsg
= `Failed to read charging station template file ${templateFile}`;
235 logger
.error(`${logPrefix} ${errorMsg}`);
236 throw new BaseError(errorMsg
);
238 if (isEmptyObject(stationTemplate
)) {
239 const errorMsg
= `Empty charging station information from template file ${templateFile}`;
240 logger
.error(`${logPrefix} ${errorMsg}`);
241 throw new BaseError(errorMsg
);
243 if (isEmptyObject(stationTemplate
.AutomaticTransactionGenerator
!)) {
244 stationTemplate
.AutomaticTransactionGenerator
= Constants
.DEFAULT_ATG_CONFIGURATION
;
246 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
247 Constants
.DEFAULT_ATG_CONFIGURATION
,
250 if (isNullOrUndefined(stationTemplate
.idTagsFile
) || isEmptyString(stationTemplate
.idTagsFile
)) {
252 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`,
257 export const checkConfiguration
= (
258 stationConfiguration
: ChargingStationConfiguration
| undefined,
260 configurationFile
: string,
262 if (isNullOrUndefined(stationConfiguration
)) {
263 const errorMsg
= `Failed to read charging station configuration file ${configurationFile}`;
264 logger
.error(`${logPrefix} ${errorMsg}`);
265 throw new BaseError(errorMsg
);
267 if (isEmptyObject(stationConfiguration
!)) {
268 const errorMsg
= `Empty charging station configuration from file ${configurationFile}`;
269 logger
.error(`${logPrefix} ${errorMsg}`);
270 throw new BaseError(errorMsg
);
274 export const checkConnectorsConfiguration
= (
275 stationTemplate
: ChargingStationTemplate
,
277 templateFile
: string,
279 configuredMaxConnectors
: number;
280 templateMaxConnectors
: number;
281 templateMaxAvailableConnectors
: number;
283 const configuredMaxConnectors
= getConfiguredMaxNumberOfConnectors(stationTemplate
);
284 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
);
285 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
!);
286 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
);
287 const templateMaxAvailableConnectors
= stationTemplate
.Connectors
?.[0]
288 ? templateMaxConnectors
- 1
289 : templateMaxConnectors
;
291 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
292 !stationTemplate
?.randomConnectors
295 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`,
297 stationTemplate
.randomConnectors
= true;
299 return { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
};
302 export const checkStationInfoConnectorStatus
= (
304 connectorStatus
: ConnectorStatus
,
306 templateFile
: string,
308 if (!isNullOrUndefined(connectorStatus
?.status)) {
310 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`,
312 delete connectorStatus
.status;
316 export const buildConnectorsMap
= (
317 connectors
: Record
<string, ConnectorStatus
>,
319 templateFile
: string,
320 ): Map
<number, ConnectorStatus
> => {
321 const connectorsMap
= new Map
<number, ConnectorStatus
>();
322 if (getMaxNumberOfConnectors(connectors
) > 0) {
323 for (const connector
in connectors
) {
324 const connectorStatus
= connectors
[connector
];
325 const connectorId
= convertToInt(connector
);
326 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
);
327 connectorsMap
.set(connectorId
, cloneObject
<ConnectorStatus
>(connectorStatus
));
331 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`,
334 return connectorsMap
;
337 export const initializeConnectorsMapStatus
= (
338 connectors
: Map
<number, ConnectorStatus
>,
341 for (const connectorId
of connectors
.keys()) {
342 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
344 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
349 if (connectorId
=== 0) {
350 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
;
351 if (isUndefined(connectors
.get(connectorId
)?.chargingProfiles
)) {
352 connectors
.get(connectorId
)!.chargingProfiles
= [];
356 isNullOrUndefined(connectors
.get(connectorId
)?.transactionStarted
)
358 initializeConnectorStatus(connectors
.get(connectorId
)!);
363 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
364 connectorStatus
.chargingProfiles
=
365 connectorStatus
.transactionId
&& isNotEmptyArray(connectorStatus
.chargingProfiles
)
366 ? connectorStatus
.chargingProfiles
?.filter(
367 (chargingProfile
) => chargingProfile
.transactionId
!== connectorStatus
.transactionId
,
370 connectorStatus
.idTagLocalAuthorized
= false;
371 connectorStatus
.idTagAuthorized
= false;
372 connectorStatus
.transactionRemoteStarted
= false;
373 connectorStatus
.transactionStarted
= false;
374 delete connectorStatus
?.transactionStart
;
375 delete connectorStatus
?.transactionId
;
376 delete connectorStatus
?.localAuthorizeIdTag
;
377 delete connectorStatus
?.authorizeIdTag
;
378 delete connectorStatus
?.transactionIdTag
;
379 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0;
380 delete connectorStatus
?.transactionBeginMeterValue
;
383 export const createBootNotificationRequest
= (
384 stationInfo
: ChargingStationInfo
,
385 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
,
386 ): BootNotificationRequest
=> {
387 const ocppVersion
= stationInfo
.ocppVersion
!;
388 switch (ocppVersion
) {
389 case OCPPVersion
.VERSION_16
:
391 chargePointModel
: stationInfo
.chargePointModel
,
392 chargePointVendor
: stationInfo
.chargePointVendor
,
393 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
394 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
396 ...(!isUndefined(stationInfo
.chargePointSerialNumber
) && {
397 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
399 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
400 firmwareVersion
: stationInfo
.firmwareVersion
,
402 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
403 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
404 ...(!isUndefined(stationInfo
.meterSerialNumber
) && {
405 meterSerialNumber
: stationInfo
.meterSerialNumber
,
407 ...(!isUndefined(stationInfo
.meterType
) && {
408 meterType
: stationInfo
.meterType
,
410 } as OCPP16BootNotificationRequest
;
411 case OCPPVersion
.VERSION_20
:
412 case OCPPVersion
.VERSION_201
:
416 model
: stationInfo
.chargePointModel
,
417 vendorName
: stationInfo
.chargePointVendor
,
418 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
419 firmwareVersion
: stationInfo
.firmwareVersion
,
421 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
422 serialNumber
: stationInfo
.chargeBoxSerialNumber
,
424 ...((!isUndefined(stationInfo
.iccid
) || !isUndefined(stationInfo
.imsi
)) && {
426 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
427 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
431 } as OCPP20BootNotificationRequest
;
435 export const warnTemplateKeysDeprecation
= (
436 stationTemplate
: ChargingStationTemplate
,
438 templateFile
: string,
440 const templateKeys
: { deprecatedKey
: string; key
?: string }[] = [
441 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
442 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
443 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
444 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' },
446 for (const templateKey
of templateKeys
) {
447 warnDeprecatedTemplateKey(
449 templateKey
.deprecatedKey
,
452 !isUndefined(templateKey
.key
) ? `Use '${templateKey.key}' instead` : undefined,
454 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
);
458 export const stationTemplateToStationInfo
= (
459 stationTemplate
: ChargingStationTemplate
,
460 ): ChargingStationInfo
=> {
461 stationTemplate
= cloneObject
<ChargingStationTemplate
>(stationTemplate
);
462 delete stationTemplate
.power
;
463 delete stationTemplate
.powerUnit
;
464 delete stationTemplate
.Connectors
;
465 delete stationTemplate
.Evses
;
466 delete stationTemplate
.Configuration
;
467 delete stationTemplate
.AutomaticTransactionGenerator
;
468 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
469 delete stationTemplate
.chargePointSerialNumberPrefix
;
470 delete stationTemplate
.meterSerialNumberPrefix
;
471 return stationTemplate
as ChargingStationInfo
;
474 export const createSerialNumber
= (
475 stationTemplate
: ChargingStationTemplate
,
476 stationInfo
: ChargingStationInfo
,
478 randomSerialNumberUpperCase
?: boolean;
479 randomSerialNumber
?: boolean;
482 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
};
483 const serialNumberSuffix
= params
?.randomSerialNumber
484 ? getRandomSerialNumberSuffix({
485 upperCase
: params
.randomSerialNumberUpperCase
,
488 isNotEmptyString(stationTemplate
?.chargePointSerialNumberPrefix
) &&
489 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
490 isNotEmptyString(stationTemplate
?.chargeBoxSerialNumberPrefix
) &&
491 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
492 isNotEmptyString(stationTemplate
?.meterSerialNumberPrefix
) &&
493 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
496 export const propagateSerialNumber
= (
497 stationTemplate
: ChargingStationTemplate
,
498 stationInfoSrc
: ChargingStationInfo
,
499 stationInfoDst
: ChargingStationInfo
,
501 if (!stationInfoSrc
|| !stationTemplate
) {
503 'Missing charging station template or existing configuration to propagate serial number',
506 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
507 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
508 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
509 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
510 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
511 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
512 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
513 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
514 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
517 export const hasFeatureProfile
= (
518 chargingStation
: ChargingStation
,
519 featureProfile
: SupportedFeatureProfiles
,
520 ): boolean | undefined => {
521 return getConfigurationKey(
523 StandardParametersKey
.SupportedFeatureProfiles
,
524 )?.value
?.includes(featureProfile
);
527 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
529 switch (stationInfo
.amperageLimitationUnit
) {
530 case AmpereUnits
.DECI_AMPERE
:
533 case AmpereUnits
.CENTI_AMPERE
:
536 case AmpereUnits
.MILLI_AMPERE
:
544 * Gets the connector cloned charging profiles applying a power limitation
545 * and sorted by connector id descending then stack level descending
547 * @param chargingStation -
548 * @param connectorId -
549 * @returns connector charging profiles array
551 export const getConnectorChargingProfiles
= (
552 chargingStation
: ChargingStation
,
555 return cloneObject
<ChargingProfile
[]>(
556 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
557 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
559 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? []).sort(
560 (a
, b
) => b
.stackLevel
- a
.stackLevel
,
566 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
567 chargingStation
: ChargingStation
,
569 ): number | undefined => {
570 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined;
571 // Get charging profiles sorted by connector id then stack level
572 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
);
573 if (isNotEmptyArray(chargingProfiles
)) {
574 const result
= getLimitFromChargingProfiles(
578 chargingStation
.logPrefix(),
580 if (!isNullOrUndefined(result
)) {
581 limit
= result
?.limit
;
582 chargingProfile
= result
?.chargingProfile
;
583 switch (chargingStation
.stationInfo
?.currentOutType
) {
586 chargingProfile
?.chargingSchedule
?.chargingRateUnit
=== ChargingRateUnitType
.WATT
588 : ACElectricUtils
.powerTotal(
589 chargingStation
.getNumberOfPhases(),
590 chargingStation
.stationInfo
.voltageOut
!,
596 chargingProfile
?.chargingSchedule
?.chargingRateUnit
=== ChargingRateUnitType
.WATT
598 : DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
!);
600 const connectorMaximumPower
=
601 chargingStation
.stationInfo
.maximumPower
! / chargingStation
.powerDivider
;
602 if (limit
! > connectorMaximumPower
) {
604 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${chargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
607 limit
= connectorMaximumPower
;
614 export const getDefaultVoltageOut
= (
615 currentType
: CurrentType
,
617 templateFile
: string,
619 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
620 let defaultVoltageOut
: number;
621 switch (currentType
) {
623 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
626 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
629 logger
.error(`${logPrefix} ${errorMsg}`);
630 throw new BaseError(errorMsg
);
632 return defaultVoltageOut
;
635 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
637 stationInfo
.idTagsFile
&&
638 join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
642 export const waitChargingStationEvents
= async (
643 emitter
: EventEmitter
,
644 event
: ChargingStationWorkerMessageEvents
,
645 eventsToWait
: number,
646 ): Promise
<number> => {
647 return new Promise
<number>((resolve
) => {
649 if (eventsToWait
=== 0) {
652 emitter
.on(event
, () => {
654 if (events
=== eventsToWait
) {
661 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
662 let configuredMaxNumberOfConnectors
= 0;
663 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
) === true) {
664 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
665 configuredMaxNumberOfConnectors
=
666 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)];
667 } else if (isUndefined(stationTemplate
.numberOfConnectors
) === false) {
668 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
as number;
669 } else if (stationTemplate
.Connectors
&& !stationTemplate
.Evses
) {
670 configuredMaxNumberOfConnectors
= stationTemplate
.Connectors
?.[0]
671 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
672 : getMaxNumberOfConnectors(stationTemplate
.Connectors
);
673 } else if (stationTemplate
.Evses
&& !stationTemplate
.Connectors
) {
674 for (const evse
in stationTemplate
.Evses
) {
678 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
679 stationTemplate
.Evses
[evse
].Connectors
,
683 return configuredMaxNumberOfConnectors
;
686 const checkConfiguredMaxConnectors
= (
687 configuredMaxConnectors
: number,
689 templateFile
: string,
691 if (configuredMaxConnectors
<= 0) {
693 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
698 const checkTemplateMaxConnectors
= (
699 templateMaxConnectors
: number,
701 templateFile
: string,
703 if (templateMaxConnectors
=== 0) {
705 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
707 } else if (templateMaxConnectors
< 0) {
709 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
714 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
715 connectorStatus
.availability
= AvailabilityType
.Operative
;
716 connectorStatus
.idTagLocalAuthorized
= false;
717 connectorStatus
.idTagAuthorized
= false;
718 connectorStatus
.transactionRemoteStarted
= false;
719 connectorStatus
.transactionStarted
= false;
720 connectorStatus
.energyActiveImportRegisterValue
= 0;
721 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0;
722 if (isUndefined(connectorStatus
.chargingProfiles
)) {
723 connectorStatus
.chargingProfiles
= [];
727 const warnDeprecatedTemplateKey
= (
728 template
: ChargingStationTemplate
,
731 templateFile
: string,
734 if (!isUndefined(template
?.[key
as keyof ChargingStationTemplate
])) {
735 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
736 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
738 logger
.warn(`${logPrefix} ${logMsg}`);
739 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`);
743 const convertDeprecatedTemplateKey
= (
744 template
: ChargingStationTemplate
,
745 deprecatedKey
: string,
748 if (!isUndefined(template
?.[deprecatedKey
as keyof ChargingStationTemplate
])) {
749 if (!isUndefined(key
)) {
750 (template
as unknown
as Record
<string, unknown
>)[key
!] =
751 template
[deprecatedKey
as keyof ChargingStationTemplate
];
753 delete template
[deprecatedKey
as keyof ChargingStationTemplate
];
757 interface ChargingProfilesLimit
{
759 chargingProfile
: ChargingProfile
;
763 * Charging profiles shall already be sorted by connector id descending then stack level descending
765 * @param chargingStation -
766 * @param connectorId -
767 * @param chargingProfiles -
769 * @returns ChargingProfilesLimit
771 const getLimitFromChargingProfiles
= (
772 chargingStation
: ChargingStation
,
774 chargingProfiles
: ChargingProfile
[],
776 ): ChargingProfilesLimit
| undefined => {
777 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
778 const currentDate
= new Date();
779 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)!;
780 for (const chargingProfile
of chargingProfiles
) {
781 const chargingSchedule
= chargingProfile
.chargingSchedule
;
782 if (isNullOrUndefined(chargingSchedule
?.startSchedule
) && connectorStatus
?.transactionStarted
) {
784 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`,
786 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
787 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
;
790 !isNullOrUndefined(chargingSchedule
?.startSchedule
) &&
791 !isDate(chargingSchedule
?.startSchedule
)
794 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`,
796 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
?.startSchedule
)!;
799 !isNullOrUndefined(chargingSchedule
?.startSchedule
) &&
800 isNullOrUndefined(chargingSchedule
?.duration
)
803 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`,
805 // OCPP specifies that if duration is not defined, it should be infinite
806 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
!);
808 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
811 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
814 // Check if the charging profile is active
816 isWithinInterval(currentDate
, {
817 start
: chargingSchedule
.startSchedule
!,
818 end
: addSeconds(chargingSchedule
.startSchedule
!, chargingSchedule
.duration
!),
821 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
822 const chargingSchedulePeriodCompareFn
= (
823 a
: ChargingSchedulePeriod
,
824 b
: ChargingSchedulePeriod
,
825 ) => a
.startPeriod
- b
.startPeriod
;
827 !isArraySorted
<ChargingSchedulePeriod
>(
828 chargingSchedule
.chargingSchedulePeriod
,
829 chargingSchedulePeriodCompareFn
,
833 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
835 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
);
837 // Check if the first schedule period startPeriod property is equal to 0
838 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
840 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`,
844 // Handle only one schedule period
845 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
846 const result
: ChargingProfilesLimit
= {
847 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
850 logger
.debug(debugLogMsg
, result
);
853 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined;
854 // Search for the right schedule period
857 chargingSchedulePeriod
,
858 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
859 // Find the right schedule period
862 addSeconds(chargingSchedule
.startSchedule
!, chargingSchedulePeriod
.startPeriod
),
866 // Found the schedule period: previous is the correct one
867 const result
: ChargingProfilesLimit
= {
868 limit
: previousChargingSchedulePeriod
!.limit
,
871 logger
.debug(debugLogMsg
, result
);
874 // Keep a reference to previous one
875 previousChargingSchedulePeriod
= chargingSchedulePeriod
;
876 // Handle the last schedule period within the charging profile duration
878 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
879 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
882 chargingSchedule
.startSchedule
!,
883 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
,
885 chargingSchedule
.startSchedule
!,
886 ) > chargingSchedule
.duration
!)
888 const result
: ChargingProfilesLimit
= {
889 limit
: previousChargingSchedulePeriod
.limit
,
892 logger
.debug(debugLogMsg
, result
);
901 export const prepareChargingProfileKind
= (
902 connectorStatus
: ConnectorStatus
,
903 chargingProfile
: ChargingProfile
,
907 switch (chargingProfile
.chargingProfileKind
) {
908 case ChargingProfileKindType
.RECURRING
:
909 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
912 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
);
914 case ChargingProfileKindType
.RELATIVE
:
915 if (!isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
)) {
917 `${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`,
919 delete chargingProfile
.chargingSchedule
.startSchedule
;
921 if (connectorStatus
?.transactionStarted
) {
922 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
;
924 // FIXME: Handle relative charging profile duration
930 export const canProceedChargingProfile
= (
931 chargingProfile
: ChargingProfile
,
936 (isValidTime(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
!)) ||
937 (isValidTime(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
!))
940 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
941 chargingProfile.chargingProfileId
942 } is not valid for the current date ${currentDate.toISOString()}`,
947 isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
) ||
948 isNullOrUndefined(chargingProfile
.chargingSchedule
.duration
)
951 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`,
956 !isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
) &&
957 !isValidTime(chargingProfile
.chargingSchedule
.startSchedule
)
960 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`,
965 !isNullOrUndefined(chargingProfile
.chargingSchedule
.duration
) &&
966 !Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)
969 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`,
976 const canProceedRecurringChargingProfile
= (
977 chargingProfile
: ChargingProfile
,
981 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
982 isNullOrUndefined(chargingProfile
.recurrencyKind
)
985 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
990 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
991 isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
)
994 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`,
1002 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1004 * @param chargingProfile -
1005 * @param currentDate -
1006 * @param logPrefix -
1008 const prepareRecurringChargingProfile
= (
1009 chargingProfile
: ChargingProfile
,
1013 const chargingSchedule
= chargingProfile
.chargingSchedule
;
1014 let recurringIntervalTranslated
= false;
1015 let recurringInterval
: Interval
;
1016 switch (chargingProfile
.recurrencyKind
) {
1017 case RecurrencyKindType
.DAILY
:
1018 recurringInterval
= {
1019 start
: chargingSchedule
.startSchedule
!,
1020 end
: addDays(chargingSchedule
.startSchedule
!, 1),
1022 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
);
1024 !isWithinInterval(currentDate
, recurringInterval
) &&
1025 isBefore(recurringInterval
.end
, currentDate
)
1027 chargingSchedule
.startSchedule
= addDays(
1028 recurringInterval
.start
,
1029 differenceInDays(currentDate
, recurringInterval
.start
),
1031 recurringInterval
= {
1032 start
: chargingSchedule
.startSchedule
,
1033 end
: addDays(chargingSchedule
.startSchedule
, 1),
1035 recurringIntervalTranslated
= true;
1038 case RecurrencyKindType
.WEEKLY
:
1039 recurringInterval
= {
1040 start
: chargingSchedule
.startSchedule
!,
1041 end
: addWeeks(chargingSchedule
.startSchedule
!, 1),
1043 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
);
1045 !isWithinInterval(currentDate
, recurringInterval
) &&
1046 isBefore(recurringInterval
.end
, currentDate
)
1048 chargingSchedule
.startSchedule
= addWeeks(
1049 recurringInterval
.start
,
1050 differenceInWeeks(currentDate
, recurringInterval
.start
),
1052 recurringInterval
= {
1053 start
: chargingSchedule
.startSchedule
,
1054 end
: addWeeks(chargingSchedule
.startSchedule
, 1),
1056 recurringIntervalTranslated
= true;
1061 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`,
1064 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1066 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1067 chargingProfile.recurrencyKind
1068 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1069 recurringInterval!.start,
1070 ).toISOString()}, ${toDate(
1071 recurringInterval!.end,
1072 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
1075 return recurringIntervalTranslated
;
1078 const checkRecurringChargingProfileDuration
= (
1079 chargingProfile
: ChargingProfile
,
1083 if (isNullOrUndefined(chargingProfile
.chargingSchedule
.duration
)) {
1085 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1086 chargingProfile.chargingProfileKind
1087 } charging profile id ${
1088 chargingProfile.chargingProfileId
1089 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1094 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
);
1096 chargingProfile
.chargingSchedule
.duration
! > differenceInSeconds(interval
.end
, interval
.start
)
1099 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1100 chargingProfile.chargingProfileKind
1101 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1102 chargingProfile.chargingSchedule.duration
1103 } is greater than the recurrency time interval duration ${differenceInSeconds(
1108 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
);
1112 const getRandomSerialNumberSuffix
= (params
?: {
1113 randomBytesLength
?: number;
1114 upperCase
?: boolean;
1116 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex');
1117 if (params
?.upperCase
) {
1118 return randomSerialNumberSuffix
.toUpperCase();
1120 return randomSerialNumberSuffix
;