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';
23 import { maxTime
} from
'date-fns/constants';
25 import type { ChargingStation
} from
'./ChargingStation';
26 import { getConfigurationKey
} from
'./ConfigurationKeyUtils';
27 import { BaseError
} from
'../exception';
31 type BootNotificationRequest
,
34 ChargingProfileKindType
,
36 type ChargingSchedulePeriod
,
37 type ChargingStationConfiguration
,
38 type ChargingStationInfo
,
39 type ChargingStationTemplate
,
40 ChargingStationWorkerMessageEvents
,
41 ConnectorPhaseRotation
,
46 type OCPP16BootNotificationRequest
,
47 type OCPP20BootNotificationRequest
,
51 ReservationTerminationReason
,
52 StandardParametersKey
,
53 SupportedFeatureProfiles
,
75 const moduleName
= 'Helpers';
77 export const getChargingStationId
= (
79 stationTemplate
: ChargingStationTemplate
| undefined,
81 if (stationTemplate
=== undefined) {
82 return "Unknown 'chargingStationId'";
84 // In case of multiple instances: add instance index to charging station id
85 const instanceIndex
= env
.CF_INSTANCE_INDEX
?? 0;
86 const idSuffix
= stationTemplate
?.nameSuffix
?? '';
87 const idStr
= `000000000${index.toString()}`;
88 return stationTemplate
?.fixedName
89 ? stationTemplate
.baseName
90 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
95 export const hasReservationExpired
= (reservation
: Reservation
): boolean => {
96 return isPast(reservation
.expiryDate
);
99 export const removeExpiredReservations
= async (
100 chargingStation
: ChargingStation
,
101 ): Promise
<void> => {
102 if (chargingStation
.hasEvses
) {
103 for (const evseStatus
of chargingStation
.evses
.values()) {
104 for (const connectorStatus
of evseStatus
.connectors
.values()) {
105 if (connectorStatus
.reservation
&& hasReservationExpired(connectorStatus
.reservation
)) {
106 await chargingStation
.removeReservation(
107 connectorStatus
.reservation
,
108 ReservationTerminationReason
.EXPIRED
,
114 for (const connectorStatus
of chargingStation
.connectors
.values()) {
115 if (connectorStatus
.reservation
&& hasReservationExpired(connectorStatus
.reservation
)) {
116 await chargingStation
.removeReservation(
117 connectorStatus
.reservation
,
118 ReservationTerminationReason
.EXPIRED
,
125 export const getNumberOfReservableConnectors
= (
126 connectors
: Map
<number, ConnectorStatus
>,
128 let numberOfReservableConnectors
= 0;
129 for (const [connectorId
, connectorStatus
] of connectors
) {
130 if (connectorId
=== 0) {
133 if (connectorStatus
.status === ConnectorStatusEnum
.Available
) {
134 ++numberOfReservableConnectors
;
137 return numberOfReservableConnectors
;
140 export const getHashId
= (index
: number, stationTemplate
: ChargingStationTemplate
): string => {
141 const chargingStationInfo
= {
142 chargePointModel
: stationTemplate
.chargePointModel
,
143 chargePointVendor
: stationTemplate
.chargePointVendor
,
144 ...(!isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
145 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
147 ...(!isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
148 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
150 ...(!isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
151 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
153 ...(!isUndefined(stationTemplate
.meterType
) && {
154 meterType
: stationTemplate
.meterType
,
157 return createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
158 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
162 export const checkChargingStation
= (
163 chargingStation
: ChargingStation
,
166 if (chargingStation
.started
=== false && chargingStation
.starting
=== false) {
167 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`);
173 export const getPhaseRotationValue
= (
175 numberOfPhases
: number,
176 ): string | undefined => {
178 if (connectorId
=== 0 && numberOfPhases
=== 0) {
179 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
180 } else if (connectorId
> 0 && numberOfPhases
=== 0) {
181 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
183 } else if (connectorId
>= 0 && numberOfPhases
=== 1) {
184 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
185 } else if (connectorId
>= 0 && numberOfPhases
=== 3) {
186 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
190 export const getMaxNumberOfEvses
= (evses
: Record
<string, EvseTemplate
>): number => {
194 return Object.keys(evses
).length
;
197 const getMaxNumberOfConnectors
= (connectors
: Record
<string, ConnectorStatus
>): number => {
201 return Object.keys(connectors
).length
;
204 export const getBootConnectorStatus
= (
205 chargingStation
: ChargingStation
,
207 connectorStatus
: ConnectorStatus
,
208 ): ConnectorStatusEnum
=> {
209 let connectorBootStatus
: ConnectorStatusEnum
;
211 !connectorStatus
?.status &&
212 (chargingStation
.isChargingStationAvailable() === false ||
213 chargingStation
.isConnectorAvailable(connectorId
) === false)
215 connectorBootStatus
= ConnectorStatusEnum
.Unavailable
;
216 } else if (!connectorStatus
?.status && connectorStatus
?.bootStatus
) {
217 // Set boot status in template at startup
218 connectorBootStatus
= connectorStatus
?.bootStatus
;
219 } else if (connectorStatus
?.status) {
220 // Set previous status at startup
221 connectorBootStatus
= connectorStatus
?.status;
223 // Set default status
224 connectorBootStatus
= ConnectorStatusEnum
.Available
;
226 return connectorBootStatus
;
229 export const checkTemplate
= (
230 stationTemplate
: ChargingStationTemplate
,
232 templateFile
: string,
234 if (isNullOrUndefined(stationTemplate
)) {
235 const errorMsg
= `Failed to read charging station template file ${templateFile}`;
236 logger
.error(`${logPrefix} ${errorMsg}`);
237 throw new BaseError(errorMsg
);
239 if (isEmptyObject(stationTemplate
)) {
240 const errorMsg
= `Empty charging station information from template file ${templateFile}`;
241 logger
.error(`${logPrefix} ${errorMsg}`);
242 throw new BaseError(errorMsg
);
244 if (isEmptyObject(stationTemplate
.AutomaticTransactionGenerator
!)) {
245 stationTemplate
.AutomaticTransactionGenerator
= Constants
.DEFAULT_ATG_CONFIGURATION
;
247 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
248 Constants
.DEFAULT_ATG_CONFIGURATION
,
251 if (isNullOrUndefined(stationTemplate
.idTagsFile
) || isEmptyString(stationTemplate
.idTagsFile
)) {
253 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`,
258 export const checkConfiguration
= (
259 stationConfiguration
: ChargingStationConfiguration
| undefined,
261 configurationFile
: string,
263 if (isNullOrUndefined(stationConfiguration
)) {
264 const errorMsg
= `Failed to read charging station configuration file ${configurationFile}`;
265 logger
.error(`${logPrefix} ${errorMsg}`);
266 throw new BaseError(errorMsg
);
268 if (isEmptyObject(stationConfiguration
!)) {
269 const errorMsg
= `Empty charging station configuration from file ${configurationFile}`;
270 logger
.error(`${logPrefix} ${errorMsg}`);
271 throw new BaseError(errorMsg
);
275 export const checkConnectorsConfiguration
= (
276 stationTemplate
: ChargingStationTemplate
,
278 templateFile
: string,
280 configuredMaxConnectors
: number;
281 templateMaxConnectors
: number;
282 templateMaxAvailableConnectors
: number;
284 const configuredMaxConnectors
= getConfiguredMaxNumberOfConnectors(stationTemplate
);
285 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
);
286 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
!);
287 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
);
288 const templateMaxAvailableConnectors
= stationTemplate
.Connectors
?.[0]
289 ? templateMaxConnectors
- 1
290 : templateMaxConnectors
;
292 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
293 !stationTemplate
?.randomConnectors
296 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`,
298 stationTemplate
.randomConnectors
= true;
300 return { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
};
303 export const checkStationInfoConnectorStatus
= (
305 connectorStatus
: ConnectorStatus
,
307 templateFile
: string,
309 if (!isNullOrUndefined(connectorStatus
?.status)) {
311 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`,
313 delete connectorStatus
.status;
317 export const buildConnectorsMap
= (
318 connectors
: Record
<string, ConnectorStatus
>,
320 templateFile
: string,
321 ): Map
<number, ConnectorStatus
> => {
322 const connectorsMap
= new Map
<number, ConnectorStatus
>();
323 if (getMaxNumberOfConnectors(connectors
) > 0) {
324 for (const connector
in connectors
) {
325 const connectorStatus
= connectors
[connector
];
326 const connectorId
= convertToInt(connector
);
327 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
);
328 connectorsMap
.set(connectorId
, cloneObject
<ConnectorStatus
>(connectorStatus
));
332 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`,
335 return connectorsMap
;
338 export const initializeConnectorsMapStatus
= (
339 connectors
: Map
<number, ConnectorStatus
>,
342 for (const connectorId
of connectors
.keys()) {
343 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
345 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
350 if (connectorId
=== 0) {
351 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
;
352 if (isUndefined(connectors
.get(connectorId
)?.chargingProfiles
)) {
353 connectors
.get(connectorId
)!.chargingProfiles
= [];
357 isNullOrUndefined(connectors
.get(connectorId
)?.transactionStarted
)
359 initializeConnectorStatus(connectors
.get(connectorId
)!);
364 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
365 connectorStatus
.chargingProfiles
=
366 connectorStatus
.transactionId
&& isNotEmptyArray(connectorStatus
.chargingProfiles
)
367 ? connectorStatus
.chargingProfiles
?.filter(
368 (chargingProfile
) => chargingProfile
.transactionId
!== connectorStatus
.transactionId
,
371 connectorStatus
.idTagLocalAuthorized
= false;
372 connectorStatus
.idTagAuthorized
= false;
373 connectorStatus
.transactionRemoteStarted
= false;
374 connectorStatus
.transactionStarted
= false;
375 delete connectorStatus
?.transactionStart
;
376 delete connectorStatus
?.transactionId
;
377 delete connectorStatus
?.localAuthorizeIdTag
;
378 delete connectorStatus
?.authorizeIdTag
;
379 delete connectorStatus
?.transactionIdTag
;
380 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0;
381 delete connectorStatus
?.transactionBeginMeterValue
;
384 export const createBootNotificationRequest
= (
385 stationInfo
: ChargingStationInfo
,
386 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
,
387 ): BootNotificationRequest
=> {
388 const ocppVersion
= stationInfo
.ocppVersion
!;
389 switch (ocppVersion
) {
390 case OCPPVersion
.VERSION_16
:
392 chargePointModel
: stationInfo
.chargePointModel
,
393 chargePointVendor
: stationInfo
.chargePointVendor
,
394 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
395 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
397 ...(!isUndefined(stationInfo
.chargePointSerialNumber
) && {
398 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
400 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
401 firmwareVersion
: stationInfo
.firmwareVersion
,
403 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
404 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
405 ...(!isUndefined(stationInfo
.meterSerialNumber
) && {
406 meterSerialNumber
: stationInfo
.meterSerialNumber
,
408 ...(!isUndefined(stationInfo
.meterType
) && {
409 meterType
: stationInfo
.meterType
,
411 } as OCPP16BootNotificationRequest
;
412 case OCPPVersion
.VERSION_20
:
413 case OCPPVersion
.VERSION_201
:
417 model
: stationInfo
.chargePointModel
,
418 vendorName
: stationInfo
.chargePointVendor
,
419 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
420 firmwareVersion
: stationInfo
.firmwareVersion
,
422 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
423 serialNumber
: stationInfo
.chargeBoxSerialNumber
,
425 ...((!isUndefined(stationInfo
.iccid
) || !isUndefined(stationInfo
.imsi
)) && {
427 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
428 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
432 } as OCPP20BootNotificationRequest
;
436 export const warnTemplateKeysDeprecation
= (
437 stationTemplate
: ChargingStationTemplate
,
439 templateFile
: string,
441 const templateKeys
: { deprecatedKey
: string; key
?: string }[] = [
442 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
443 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
444 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
445 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' },
447 for (const templateKey
of templateKeys
) {
448 warnDeprecatedTemplateKey(
450 templateKey
.deprecatedKey
,
453 !isUndefined(templateKey
.key
) ? `Use '${templateKey.key}' instead` : undefined,
455 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
);
459 export const stationTemplateToStationInfo
= (
460 stationTemplate
: ChargingStationTemplate
,
461 ): ChargingStationInfo
=> {
462 stationTemplate
= cloneObject
<ChargingStationTemplate
>(stationTemplate
);
463 delete stationTemplate
.power
;
464 delete stationTemplate
.powerUnit
;
465 delete stationTemplate
.Connectors
;
466 delete stationTemplate
.Evses
;
467 delete stationTemplate
.Configuration
;
468 delete stationTemplate
.AutomaticTransactionGenerator
;
469 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
470 delete stationTemplate
.chargePointSerialNumberPrefix
;
471 delete stationTemplate
.meterSerialNumberPrefix
;
472 return stationTemplate
as ChargingStationInfo
;
475 export const createSerialNumber
= (
476 stationTemplate
: ChargingStationTemplate
,
477 stationInfo
: ChargingStationInfo
,
479 randomSerialNumberUpperCase
?: boolean;
480 randomSerialNumber
?: boolean;
483 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
};
484 const serialNumberSuffix
= params
?.randomSerialNumber
485 ? getRandomSerialNumberSuffix({
486 upperCase
: params
.randomSerialNumberUpperCase
,
489 isNotEmptyString(stationTemplate
?.chargePointSerialNumberPrefix
) &&
490 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
491 isNotEmptyString(stationTemplate
?.chargeBoxSerialNumberPrefix
) &&
492 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
493 isNotEmptyString(stationTemplate
?.meterSerialNumberPrefix
) &&
494 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
497 export const propagateSerialNumber
= (
498 stationTemplate
: ChargingStationTemplate
,
499 stationInfoSrc
: ChargingStationInfo
,
500 stationInfoDst
: ChargingStationInfo
,
502 if (!stationInfoSrc
|| !stationTemplate
) {
504 'Missing charging station template or existing configuration to propagate serial number',
507 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
508 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
509 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
510 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
511 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
512 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
513 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
514 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
515 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
518 export const hasFeatureProfile
= (
519 chargingStation
: ChargingStation
,
520 featureProfile
: SupportedFeatureProfiles
,
521 ): boolean | undefined => {
522 return getConfigurationKey(
524 StandardParametersKey
.SupportedFeatureProfiles
,
525 )?.value
?.includes(featureProfile
);
528 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
530 switch (stationInfo
.amperageLimitationUnit
) {
531 case AmpereUnits
.DECI_AMPERE
:
534 case AmpereUnits
.CENTI_AMPERE
:
537 case AmpereUnits
.MILLI_AMPERE
:
545 * Gets the connector cloned charging profiles applying a power limitation
546 * and sorted by connector id descending then stack level descending
548 * @param chargingStation -
549 * @param connectorId -
550 * @returns connector charging profiles array
552 export const getConnectorChargingProfiles
= (
553 chargingStation
: ChargingStation
,
556 return cloneObject
<ChargingProfile
[]>(
557 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
558 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
560 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? []).sort(
561 (a
, b
) => b
.stackLevel
- a
.stackLevel
,
567 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
568 chargingStation
: ChargingStation
,
570 ): number | undefined => {
571 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined;
572 // Get charging profiles sorted by connector id then stack level
573 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
);
574 if (isNotEmptyArray(chargingProfiles
)) {
575 const result
= getLimitFromChargingProfiles(
579 chargingStation
.logPrefix(),
581 if (!isNullOrUndefined(result
)) {
582 limit
= result
?.limit
;
583 chargingProfile
= result
?.chargingProfile
;
584 switch (chargingStation
.stationInfo
?.currentOutType
) {
587 chargingProfile
?.chargingSchedule
?.chargingRateUnit
=== ChargingRateUnitType
.WATT
589 : ACElectricUtils
.powerTotal(
590 chargingStation
.getNumberOfPhases(),
591 chargingStation
.stationInfo
.voltageOut
!,
597 chargingProfile
?.chargingSchedule
?.chargingRateUnit
=== ChargingRateUnitType
.WATT
599 : DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
!);
601 const connectorMaximumPower
=
602 chargingStation
.stationInfo
.maximumPower
! / chargingStation
.powerDivider
;
603 if (limit
! > connectorMaximumPower
) {
605 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${chargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
608 limit
= connectorMaximumPower
;
615 export const getDefaultVoltageOut
= (
616 currentType
: CurrentType
,
618 templateFile
: string,
620 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
621 let defaultVoltageOut
: number;
622 switch (currentType
) {
624 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
627 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
630 logger
.error(`${logPrefix} ${errorMsg}`);
631 throw new BaseError(errorMsg
);
633 return defaultVoltageOut
;
636 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
638 stationInfo
.idTagsFile
&&
639 join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
643 export const waitChargingStationEvents
= async (
644 emitter
: EventEmitter
,
645 event
: ChargingStationWorkerMessageEvents
,
646 eventsToWait
: number,
647 ): Promise
<number> => {
648 return new Promise
<number>((resolve
) => {
650 if (eventsToWait
=== 0) {
654 emitter
.on(event
, () => {
656 if (events
=== eventsToWait
) {
663 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
664 let configuredMaxNumberOfConnectors
= 0;
665 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
) === true) {
666 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
667 configuredMaxNumberOfConnectors
=
668 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)];
669 } else if (isUndefined(stationTemplate
.numberOfConnectors
) === false) {
670 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
as number;
671 } else if (stationTemplate
.Connectors
&& !stationTemplate
.Evses
) {
672 configuredMaxNumberOfConnectors
= stationTemplate
.Connectors
?.[0]
673 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
674 : getMaxNumberOfConnectors(stationTemplate
.Connectors
);
675 } else if (stationTemplate
.Evses
&& !stationTemplate
.Connectors
) {
676 for (const evse
in stationTemplate
.Evses
) {
680 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
681 stationTemplate
.Evses
[evse
].Connectors
,
685 return configuredMaxNumberOfConnectors
;
688 const checkConfiguredMaxConnectors
= (
689 configuredMaxConnectors
: number,
691 templateFile
: string,
693 if (configuredMaxConnectors
<= 0) {
695 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
700 const checkTemplateMaxConnectors
= (
701 templateMaxConnectors
: number,
703 templateFile
: string,
705 if (templateMaxConnectors
=== 0) {
707 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
709 } else if (templateMaxConnectors
< 0) {
711 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
716 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
717 connectorStatus
.availability
= AvailabilityType
.Operative
;
718 connectorStatus
.idTagLocalAuthorized
= false;
719 connectorStatus
.idTagAuthorized
= false;
720 connectorStatus
.transactionRemoteStarted
= false;
721 connectorStatus
.transactionStarted
= false;
722 connectorStatus
.energyActiveImportRegisterValue
= 0;
723 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0;
724 if (isUndefined(connectorStatus
.chargingProfiles
)) {
725 connectorStatus
.chargingProfiles
= [];
729 const warnDeprecatedTemplateKey
= (
730 template
: ChargingStationTemplate
,
733 templateFile
: string,
736 if (!isUndefined(template
?.[key
as keyof ChargingStationTemplate
])) {
737 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
738 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
740 logger
.warn(`${logPrefix} ${logMsg}`);
741 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`);
745 const convertDeprecatedTemplateKey
= (
746 template
: ChargingStationTemplate
,
747 deprecatedKey
: string,
750 if (!isUndefined(template
?.[deprecatedKey
as keyof ChargingStationTemplate
])) {
751 if (!isUndefined(key
)) {
752 (template
as unknown
as Record
<string, unknown
>)[key
!] =
753 template
[deprecatedKey
as keyof ChargingStationTemplate
];
755 delete template
[deprecatedKey
as keyof ChargingStationTemplate
];
759 interface ChargingProfilesLimit
{
761 chargingProfile
: ChargingProfile
;
765 * Charging profiles shall already be sorted by connector id descending then stack level descending
767 * @param chargingStation -
768 * @param connectorId -
769 * @param chargingProfiles -
771 * @returns ChargingProfilesLimit
773 const getLimitFromChargingProfiles
= (
774 chargingStation
: ChargingStation
,
776 chargingProfiles
: ChargingProfile
[],
778 ): ChargingProfilesLimit
| undefined => {
779 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
780 const currentDate
= new Date();
781 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)!;
782 for (const chargingProfile
of chargingProfiles
) {
783 const chargingSchedule
= chargingProfile
.chargingSchedule
;
784 if (isNullOrUndefined(chargingSchedule
?.startSchedule
) && connectorStatus
?.transactionStarted
) {
786 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`,
788 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
789 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
;
792 !isNullOrUndefined(chargingSchedule
?.startSchedule
) &&
793 !isDate(chargingSchedule
?.startSchedule
)
796 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`,
798 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
?.startSchedule
)!;
801 !isNullOrUndefined(chargingSchedule
?.startSchedule
) &&
802 isNullOrUndefined(chargingSchedule
?.duration
)
805 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`,
807 // OCPP specifies that if duration is not defined, it should be infinite
808 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
!);
810 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
813 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
816 // Check if the charging profile is active
818 isWithinInterval(currentDate
, {
819 start
: chargingSchedule
.startSchedule
!,
820 end
: addSeconds(chargingSchedule
.startSchedule
!, chargingSchedule
.duration
!),
823 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
824 const chargingSchedulePeriodCompareFn
= (
825 a
: ChargingSchedulePeriod
,
826 b
: ChargingSchedulePeriod
,
827 ) => a
.startPeriod
- b
.startPeriod
;
829 !isArraySorted
<ChargingSchedulePeriod
>(
830 chargingSchedule
.chargingSchedulePeriod
,
831 chargingSchedulePeriodCompareFn
,
835 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
837 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
);
839 // Check if the first schedule period startPeriod property is equal to 0
840 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
842 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`,
846 // Handle only one schedule period
847 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
848 const result
: ChargingProfilesLimit
= {
849 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
852 logger
.debug(debugLogMsg
, result
);
855 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined;
856 // Search for the right schedule period
859 chargingSchedulePeriod
,
860 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
861 // Find the right schedule period
864 addSeconds(chargingSchedule
.startSchedule
!, chargingSchedulePeriod
.startPeriod
),
868 // Found the schedule period: previous is the correct one
869 const result
: ChargingProfilesLimit
= {
870 limit
: previousChargingSchedulePeriod
!.limit
,
873 logger
.debug(debugLogMsg
, result
);
876 // Keep a reference to previous one
877 previousChargingSchedulePeriod
= chargingSchedulePeriod
;
878 // Handle the last schedule period within the charging profile duration
880 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
881 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
884 chargingSchedule
.startSchedule
!,
885 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
,
887 chargingSchedule
.startSchedule
!,
888 ) > chargingSchedule
.duration
!)
890 const result
: ChargingProfilesLimit
= {
891 limit
: previousChargingSchedulePeriod
.limit
,
894 logger
.debug(debugLogMsg
, result
);
903 export const prepareChargingProfileKind
= (
904 connectorStatus
: ConnectorStatus
,
905 chargingProfile
: ChargingProfile
,
909 switch (chargingProfile
.chargingProfileKind
) {
910 case ChargingProfileKindType
.RECURRING
:
911 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
914 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
);
916 case ChargingProfileKindType
.RELATIVE
:
917 if (!isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
)) {
919 `${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`,
921 delete chargingProfile
.chargingSchedule
.startSchedule
;
923 if (connectorStatus
?.transactionStarted
) {
924 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
;
926 // FIXME: Handle relative charging profile duration
932 export const canProceedChargingProfile
= (
933 chargingProfile
: ChargingProfile
,
938 (isValidTime(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
!)) ||
939 (isValidTime(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
!))
942 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
943 chargingProfile.chargingProfileId
944 } is not valid for the current date ${currentDate.toISOString()}`,
949 isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
) ||
950 isNullOrUndefined(chargingProfile
.chargingSchedule
.duration
)
953 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`,
958 !isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
) &&
959 !isValidTime(chargingProfile
.chargingSchedule
.startSchedule
)
962 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`,
967 !isNullOrUndefined(chargingProfile
.chargingSchedule
.duration
) &&
968 !Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)
971 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`,
978 const canProceedRecurringChargingProfile
= (
979 chargingProfile
: ChargingProfile
,
983 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
984 isNullOrUndefined(chargingProfile
.recurrencyKind
)
987 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
992 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
993 isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
)
996 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`,
1004 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1006 * @param chargingProfile -
1007 * @param currentDate -
1008 * @param logPrefix -
1010 const prepareRecurringChargingProfile
= (
1011 chargingProfile
: ChargingProfile
,
1015 const chargingSchedule
= chargingProfile
.chargingSchedule
;
1016 let recurringIntervalTranslated
= false;
1017 let recurringInterval
: Interval
;
1018 switch (chargingProfile
.recurrencyKind
) {
1019 case RecurrencyKindType
.DAILY
:
1020 recurringInterval
= {
1021 start
: chargingSchedule
.startSchedule
!,
1022 end
: addDays(chargingSchedule
.startSchedule
!, 1),
1024 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
);
1026 !isWithinInterval(currentDate
, recurringInterval
) &&
1027 isBefore(recurringInterval
.end
, currentDate
)
1029 chargingSchedule
.startSchedule
= addDays(
1030 recurringInterval
.start
,
1031 differenceInDays(currentDate
, recurringInterval
.start
),
1033 recurringInterval
= {
1034 start
: chargingSchedule
.startSchedule
,
1035 end
: addDays(chargingSchedule
.startSchedule
, 1),
1037 recurringIntervalTranslated
= true;
1040 case RecurrencyKindType
.WEEKLY
:
1041 recurringInterval
= {
1042 start
: chargingSchedule
.startSchedule
!,
1043 end
: addWeeks(chargingSchedule
.startSchedule
!, 1),
1045 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
);
1047 !isWithinInterval(currentDate
, recurringInterval
) &&
1048 isBefore(recurringInterval
.end
, currentDate
)
1050 chargingSchedule
.startSchedule
= addWeeks(
1051 recurringInterval
.start
,
1052 differenceInWeeks(currentDate
, recurringInterval
.start
),
1054 recurringInterval
= {
1055 start
: chargingSchedule
.startSchedule
,
1056 end
: addWeeks(chargingSchedule
.startSchedule
, 1),
1058 recurringIntervalTranslated
= true;
1063 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`,
1066 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1068 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1069 chargingProfile.recurrencyKind
1070 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1071 recurringInterval!.start,
1072 ).toISOString()}, ${toDate(
1073 recurringInterval!.end,
1074 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
1077 return recurringIntervalTranslated
;
1080 const checkRecurringChargingProfileDuration
= (
1081 chargingProfile
: ChargingProfile
,
1085 if (isNullOrUndefined(chargingProfile
.chargingSchedule
.duration
)) {
1087 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1088 chargingProfile.chargingProfileKind
1089 } charging profile id ${
1090 chargingProfile.chargingProfileId
1091 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1096 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
);
1098 chargingProfile
.chargingSchedule
.duration
! > differenceInSeconds(interval
.end
, interval
.start
)
1101 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1102 chargingProfile.chargingProfileKind
1103 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1104 chargingProfile.chargingSchedule.duration
1105 } is greater than the recurrency time interval duration ${differenceInSeconds(
1110 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
);
1114 const getRandomSerialNumberSuffix
= (params
?: {
1115 randomBytesLength
?: number;
1116 upperCase
?: boolean;
1118 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex');
1119 if (params
?.upperCase
) {
1120 return randomSerialNumberSuffix
.toUpperCase();
1122 return randomSerialNumberSuffix
;