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';
20 import type { ChargingStation
} from
'./ChargingStation';
21 import { BaseError
} from
'../exception';
25 type BootNotificationRequest
,
28 ChargingProfileKindType
,
30 type ChargingSchedulePeriod
,
31 type ChargingStationInfo
,
32 type ChargingStationTemplate
,
33 ChargingStationWorkerMessageEvents
,
34 ConnectorPhaseRotation
,
39 type OCPP16BootNotificationRequest
,
40 type OCPP20BootNotificationRequest
,
63 const moduleName
= 'ChargingStationUtils';
65 export const getChargingStationId
= (
67 stationTemplate
: ChargingStationTemplate
,
69 // In case of multiple instances: add instance index to charging station id
70 const instanceIndex
= process
.env
.CF_INSTANCE_INDEX
?? 0;
71 const idSuffix
= stationTemplate
?.nameSuffix
?? '';
72 const idStr
= `000000000${index.toString()}`;
73 return stationTemplate
?.fixedName
74 ? stationTemplate
.baseName
75 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
80 export const countReservableConnectors
= (connectors
: Map
<number, ConnectorStatus
>) => {
81 let reservableConnectors
= 0;
82 for (const [connectorId
, connectorStatus
] of connectors
) {
83 if (connectorId
=== 0) {
86 if (connectorStatus
.status === ConnectorStatusEnum
.Available
) {
87 ++reservableConnectors
;
90 return reservableConnectors
;
93 export const getHashId
= (index
: number, stationTemplate
: ChargingStationTemplate
): string => {
94 const chargingStationInfo
= {
95 chargePointModel
: stationTemplate
.chargePointModel
,
96 chargePointVendor
: stationTemplate
.chargePointVendor
,
97 ...(!isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
98 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
100 ...(!isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
101 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
103 ...(!isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
104 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
106 ...(!isUndefined(stationTemplate
.meterType
) && {
107 meterType
: stationTemplate
.meterType
,
110 return createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
111 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
115 export const checkChargingStation
= (
116 chargingStation
: ChargingStation
,
119 if (chargingStation
.started
=== false && chargingStation
.starting
=== false) {
120 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`);
126 export const getPhaseRotationValue
= (
128 numberOfPhases
: number,
129 ): string | undefined => {
131 if (connectorId
=== 0 && numberOfPhases
=== 0) {
132 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
133 } else if (connectorId
> 0 && numberOfPhases
=== 0) {
134 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
136 } else if (connectorId
> 0 && numberOfPhases
=== 1) {
137 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
138 } else if (connectorId
> 0 && numberOfPhases
=== 3) {
139 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
143 export const getMaxNumberOfEvses
= (evses
: Record
<string, EvseTemplate
>): number => {
147 return Object.keys(evses
).length
;
150 const getMaxNumberOfConnectors
= (connectors
: Record
<string, ConnectorStatus
>): number => {
154 return Object.keys(connectors
).length
;
157 export const getBootConnectorStatus
= (
158 chargingStation
: ChargingStation
,
160 connectorStatus
: ConnectorStatus
,
161 ): ConnectorStatusEnum
=> {
162 let connectorBootStatus
: ConnectorStatusEnum
;
164 !connectorStatus
?.status &&
165 (chargingStation
.isChargingStationAvailable() === false ||
166 chargingStation
.isConnectorAvailable(connectorId
) === false)
168 connectorBootStatus
= ConnectorStatusEnum
.Unavailable
;
169 } else if (!connectorStatus
?.status && connectorStatus
?.bootStatus
) {
170 // Set boot status in template at startup
171 connectorBootStatus
= connectorStatus
?.bootStatus
;
172 } else if (connectorStatus
?.status) {
173 // Set previous status at startup
174 connectorBootStatus
= connectorStatus
?.status;
176 // Set default status
177 connectorBootStatus
= ConnectorStatusEnum
.Available
;
179 return connectorBootStatus
;
182 export const checkTemplate
= (
183 stationTemplate
: ChargingStationTemplate
,
185 templateFile
: string,
187 if (isNullOrUndefined(stationTemplate
)) {
188 const errorMsg
= `Failed to read charging station template file ${templateFile}`;
189 logger
.error(`${logPrefix} ${errorMsg}`);
190 throw new BaseError(errorMsg
);
192 if (isEmptyObject(stationTemplate
)) {
193 const errorMsg
= `Empty charging station information from template file ${templateFile}`;
194 logger
.error(`${logPrefix} ${errorMsg}`);
195 throw new BaseError(errorMsg
);
197 if (isEmptyObject(stationTemplate
.AutomaticTransactionGenerator
!)) {
198 stationTemplate
.AutomaticTransactionGenerator
= Constants
.DEFAULT_ATG_CONFIGURATION
;
200 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
201 Constants
.DEFAULT_ATG_CONFIGURATION
,
204 if (isNullOrUndefined(stationTemplate
.idTagsFile
) || isEmptyString(stationTemplate
.idTagsFile
)) {
206 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`,
211 export const checkConnectorsConfiguration
= (
212 stationTemplate
: ChargingStationTemplate
,
214 templateFile
: string,
216 configuredMaxConnectors
: number;
217 templateMaxConnectors
: number;
218 templateMaxAvailableConnectors
: number;
220 const configuredMaxConnectors
= getConfiguredNumberOfConnectors(stationTemplate
);
221 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
);
222 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
!);
223 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
);
224 const templateMaxAvailableConnectors
= stationTemplate
.Connectors
![0]
225 ? templateMaxConnectors
- 1
226 : templateMaxConnectors
;
228 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
229 !stationTemplate
?.randomConnectors
232 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`,
234 stationTemplate
.randomConnectors
= true;
236 return { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
};
239 export const checkStationInfoConnectorStatus
= (
241 connectorStatus
: ConnectorStatus
,
243 templateFile
: string,
245 if (!isNullOrUndefined(connectorStatus
?.status)) {
247 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`,
249 delete connectorStatus
.status;
253 export const buildConnectorsMap
= (
254 connectors
: Record
<string, ConnectorStatus
>,
256 templateFile
: string,
257 ): Map
<number, ConnectorStatus
> => {
258 const connectorsMap
= new Map
<number, ConnectorStatus
>();
259 if (getMaxNumberOfConnectors(connectors
) > 0) {
260 for (const connector
in connectors
) {
261 const connectorStatus
= connectors
[connector
];
262 const connectorId
= convertToInt(connector
);
263 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
);
264 connectorsMap
.set(connectorId
, cloneObject
<ConnectorStatus
>(connectorStatus
));
268 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`,
271 return connectorsMap
;
274 export const initializeConnectorsMapStatus
= (
275 connectors
: Map
<number, ConnectorStatus
>,
278 for (const connectorId
of connectors
.keys()) {
279 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
281 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
286 if (connectorId
=== 0) {
287 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
;
288 if (isUndefined(connectors
.get(connectorId
)?.chargingProfiles
)) {
289 connectors
.get(connectorId
)!.chargingProfiles
= [];
293 isNullOrUndefined(connectors
.get(connectorId
)?.transactionStarted
)
295 initializeConnectorStatus(connectors
.get(connectorId
)!);
300 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
301 connectorStatus
.idTagLocalAuthorized
= false;
302 connectorStatus
.idTagAuthorized
= false;
303 connectorStatus
.transactionRemoteStarted
= false;
304 connectorStatus
.transactionStarted
= false;
305 delete connectorStatus
?.transactionStart
;
306 delete connectorStatus
?.transactionId
;
307 delete connectorStatus
?.localAuthorizeIdTag
;
308 delete connectorStatus
?.authorizeIdTag
;
309 delete connectorStatus
?.transactionIdTag
;
310 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0;
311 delete connectorStatus
?.transactionBeginMeterValue
;
314 export const createBootNotificationRequest
= (
315 stationInfo
: ChargingStationInfo
,
316 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
,
317 ): BootNotificationRequest
=> {
318 const ocppVersion
= stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
;
319 switch (ocppVersion
) {
320 case OCPPVersion
.VERSION_16
:
322 chargePointModel
: stationInfo
.chargePointModel
,
323 chargePointVendor
: stationInfo
.chargePointVendor
,
324 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
325 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
327 ...(!isUndefined(stationInfo
.chargePointSerialNumber
) && {
328 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
330 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
331 firmwareVersion
: stationInfo
.firmwareVersion
,
333 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
334 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
335 ...(!isUndefined(stationInfo
.meterSerialNumber
) && {
336 meterSerialNumber
: stationInfo
.meterSerialNumber
,
338 ...(!isUndefined(stationInfo
.meterType
) && {
339 meterType
: stationInfo
.meterType
,
341 } as OCPP16BootNotificationRequest
;
342 case OCPPVersion
.VERSION_20
:
343 case OCPPVersion
.VERSION_201
:
347 model
: stationInfo
.chargePointModel
,
348 vendorName
: stationInfo
.chargePointVendor
,
349 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
350 firmwareVersion
: stationInfo
.firmwareVersion
,
352 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
353 serialNumber
: stationInfo
.chargeBoxSerialNumber
,
355 ...((!isUndefined(stationInfo
.iccid
) || !isUndefined(stationInfo
.imsi
)) && {
357 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
358 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
362 } as OCPP20BootNotificationRequest
;
366 export const warnTemplateKeysDeprecation
= (
367 stationTemplate
: ChargingStationTemplate
,
369 templateFile
: string,
371 const templateKeys
: { deprecatedKey
: string; key
?: string }[] = [
372 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
373 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
374 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
376 for (const templateKey
of templateKeys
) {
377 warnDeprecatedTemplateKey(
379 templateKey
.deprecatedKey
,
382 !isUndefined(templateKey
.key
) ? `Use '${templateKey.key}' instead` : undefined,
384 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
);
388 export const stationTemplateToStationInfo
= (
389 stationTemplate
: ChargingStationTemplate
,
390 ): ChargingStationInfo
=> {
391 stationTemplate
= cloneObject
<ChargingStationTemplate
>(stationTemplate
);
392 delete stationTemplate
.power
;
393 delete stationTemplate
.powerUnit
;
394 delete stationTemplate
.Connectors
;
395 delete stationTemplate
.Evses
;
396 delete stationTemplate
.Configuration
;
397 delete stationTemplate
.AutomaticTransactionGenerator
;
398 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
399 delete stationTemplate
.chargePointSerialNumberPrefix
;
400 delete stationTemplate
.meterSerialNumberPrefix
;
401 return stationTemplate
as unknown
as ChargingStationInfo
;
404 export const createSerialNumber
= (
405 stationTemplate
: ChargingStationTemplate
,
406 stationInfo
: ChargingStationInfo
,
408 randomSerialNumberUpperCase
?: boolean;
409 randomSerialNumber
?: boolean;
411 randomSerialNumberUpperCase
: true,
412 randomSerialNumber
: true,
415 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
};
416 const serialNumberSuffix
= params
?.randomSerialNumber
417 ? getRandomSerialNumberSuffix({
418 upperCase
: params
.randomSerialNumberUpperCase
,
421 isNotEmptyString(stationTemplate
?.chargePointSerialNumberPrefix
) &&
422 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
423 isNotEmptyString(stationTemplate
?.chargeBoxSerialNumberPrefix
) &&
424 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
425 isNotEmptyString(stationTemplate
?.meterSerialNumberPrefix
) &&
426 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
429 export const propagateSerialNumber
= (
430 stationTemplate
: ChargingStationTemplate
,
431 stationInfoSrc
: ChargingStationInfo
,
432 stationInfoDst
: ChargingStationInfo
,
434 if (!stationInfoSrc
|| !stationTemplate
) {
436 'Missing charging station template or existing configuration to propagate serial number',
439 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
440 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
441 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
442 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
443 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
444 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
445 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
446 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
447 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
450 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
452 switch (stationInfo
.amperageLimitationUnit
) {
453 case AmpereUnits
.DECI_AMPERE
:
456 case AmpereUnits
.CENTI_AMPERE
:
459 case AmpereUnits
.MILLI_AMPERE
:
466 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
467 chargingStation
: ChargingStation
,
469 ): number | undefined => {
470 let limit
: number | undefined, matchingChargingProfile
: ChargingProfile
| undefined;
471 // Get charging profiles for connector id and sort by stack level
472 const chargingProfiles
=
473 cloneObject
<ChargingProfile
[]>(
474 chargingStation
.getConnectorStatus(connectorId
)!.chargingProfiles
!,
475 )?.sort((a
, b
) => b
.stackLevel
- a
.stackLevel
) ?? [];
476 // Get charging profiles on connector 0 and sort by stack level
477 if (chargingStation
.getConnectorStatus(0)?.chargingProfiles
) {
478 chargingProfiles
.push(
479 ...cloneObject
<ChargingProfile
[]>(
480 chargingStation
.getConnectorStatus(0)!.chargingProfiles
!,
481 ).sort((a
, b
) => b
.stackLevel
- a
.stackLevel
),
484 if (isNotEmptyArray(chargingProfiles
)) {
485 const result
= getLimitFromChargingProfiles(
489 chargingStation
.logPrefix(),
491 if (!isNullOrUndefined(result
)) {
492 limit
= result
?.limit
;
493 matchingChargingProfile
= result
?.matchingChargingProfile
;
494 switch (chargingStation
.getCurrentOutType()) {
497 matchingChargingProfile
?.chargingSchedule
?.chargingRateUnit
===
498 ChargingRateUnitType
.WATT
500 : ACElectricUtils
.powerTotal(
501 chargingStation
.getNumberOfPhases(),
502 chargingStation
.getVoltageOut(),
508 matchingChargingProfile
?.chargingSchedule
?.chargingRateUnit
===
509 ChargingRateUnitType
.WATT
511 : DCElectricUtils
.power(chargingStation
.getVoltageOut(), limit
!);
513 const connectorMaximumPower
=
514 chargingStation
.getMaximumPower() / chargingStation
.powerDivider
;
515 if (limit
! > connectorMaximumPower
) {
517 `${chargingStation.logPrefix()} Charging profile id ${matchingChargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
520 limit
= connectorMaximumPower
;
527 export const getDefaultVoltageOut
= (
528 currentType
: CurrentType
,
530 templateFile
: string,
532 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
533 let defaultVoltageOut
: number;
534 switch (currentType
) {
536 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
539 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
542 logger
.error(`${logPrefix} ${errorMsg}`);
543 throw new BaseError(errorMsg
);
545 return defaultVoltageOut
;
548 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
550 stationInfo
.idTagsFile
&&
551 join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
555 export const waitChargingStationEvents
= async (
556 emitter
: EventEmitter
,
557 event
: ChargingStationWorkerMessageEvents
,
558 eventsToWait
: number,
559 ): Promise
<number> => {
560 return new Promise
<number>((resolve
) => {
562 if (eventsToWait
=== 0) {
565 emitter
.on(event
, () => {
567 if (events
=== eventsToWait
) {
574 const getConfiguredNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
575 let configuredMaxConnectors
= 0;
576 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
) === true) {
577 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
578 configuredMaxConnectors
=
579 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)];
580 } else if (isUndefined(stationTemplate
.numberOfConnectors
) === false) {
581 configuredMaxConnectors
= stationTemplate
.numberOfConnectors
as number;
582 } else if (stationTemplate
.Connectors
&& !stationTemplate
.Evses
) {
583 configuredMaxConnectors
= stationTemplate
.Connectors
[0]
584 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
585 : getMaxNumberOfConnectors(stationTemplate
.Connectors
);
586 } else if (stationTemplate
.Evses
&& !stationTemplate
.Connectors
) {
587 for (const evse
in stationTemplate
.Evses
) {
591 configuredMaxConnectors
+= getMaxNumberOfConnectors(stationTemplate
.Evses
[evse
].Connectors
);
594 return configuredMaxConnectors
;
597 const checkConfiguredMaxConnectors
= (
598 configuredMaxConnectors
: number,
600 templateFile
: string,
602 if (configuredMaxConnectors
<= 0) {
604 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
609 const checkTemplateMaxConnectors
= (
610 templateMaxConnectors
: number,
612 templateFile
: string,
614 if (templateMaxConnectors
=== 0) {
616 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
618 } else if (templateMaxConnectors
< 0) {
620 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
625 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
626 connectorStatus
.availability
= AvailabilityType
.Operative
;
627 connectorStatus
.idTagLocalAuthorized
= false;
628 connectorStatus
.idTagAuthorized
= false;
629 connectorStatus
.transactionRemoteStarted
= false;
630 connectorStatus
.transactionStarted
= false;
631 connectorStatus
.energyActiveImportRegisterValue
= 0;
632 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0;
633 if (isUndefined(connectorStatus
.chargingProfiles
)) {
634 connectorStatus
.chargingProfiles
= [];
638 const warnDeprecatedTemplateKey
= (
639 template
: ChargingStationTemplate
,
642 templateFile
: string,
645 if (!isUndefined(template
[key
as keyof ChargingStationTemplate
])) {
646 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
647 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
649 logger
.warn(`${logPrefix} ${logMsg}`);
650 console
.warn(chalk
.yellow(`${logMsg}`));
654 const convertDeprecatedTemplateKey
= (
655 template
: ChargingStationTemplate
,
656 deprecatedKey
: string,
659 if (!isUndefined(template
[deprecatedKey
as keyof ChargingStationTemplate
])) {
660 if (!isUndefined(key
)) {
661 (template
as unknown
as Record
<string, unknown
>)[key
!] =
662 template
[deprecatedKey
as keyof ChargingStationTemplate
];
664 delete template
[deprecatedKey
as keyof ChargingStationTemplate
];
668 interface ChargingProfilesLimit
{
670 matchingChargingProfile
: ChargingProfile
;
674 * Charging profiles should already be sorted by connector id and stack level (highest stack level has priority)
676 * @param chargingProfiles -
678 * @returns ChargingProfilesLimit
680 const getLimitFromChargingProfiles
= (
681 chargingStation
: ChargingStation
,
683 chargingProfiles
: ChargingProfile
[],
685 ): ChargingProfilesLimit
| undefined => {
686 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
687 const currentDate
= new Date();
688 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
);
689 for (const chargingProfile
of chargingProfiles
) {
691 isValidDate(chargingProfile
.validFrom
) &&
692 isValidDate(chargingProfile
.validTo
) &&
693 !isWithinInterval(currentDate
, {
694 start
: chargingProfile
.validFrom
!,
695 end
: chargingProfile
.validTo
!,
699 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${
700 chargingProfile.chargingProfileId
701 } is not valid for the current date ${currentDate.toISOString()}`,
705 const chargingSchedule
= chargingProfile
.chargingSchedule
;
706 if (connectorStatus
?.transactionStarted
&& !chargingSchedule
?.startSchedule
) {
708 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}. Trying to set it to the connector transaction start date`,
710 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
711 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
;
713 if (!(chargingSchedule
?.startSchedule
instanceof Date)) {
715 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not a Date object in charging profile id ${chargingProfile.chargingProfileId}. Trying to convert it to a Date object`,
717 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!;
720 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
721 isNullOrUndefined(chargingProfile
.recurrencyKind
)
724 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
728 if (chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
) {
729 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
);
731 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RELATIVE
&&
732 connectorStatus
?.transactionStarted
734 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
;
736 // Check if the charging profile is active
738 isValidDate(chargingSchedule
.startSchedule
) &&
739 isAfter(addSeconds(chargingSchedule
.startSchedule
!, chargingSchedule
.duration
!), currentDate
)
741 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
742 // Handling of only one schedule period
744 chargingSchedule
.chargingSchedulePeriod
.length
=== 1 &&
745 chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
=== 0
747 const result
: ChargingProfilesLimit
= {
748 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
749 matchingChargingProfile
: chargingProfile
,
751 logger
.debug(debugLogMsg
, result
);
754 let lastButOneSchedule
: ChargingSchedulePeriod
| undefined;
755 // Search for the right schedule period
756 for (const schedulePeriod
of chargingSchedule
.chargingSchedulePeriod
) {
757 // Find the right schedule period
760 addSeconds(chargingSchedule
.startSchedule
!, schedulePeriod
.startPeriod
),
764 // Found the schedule period: last but one is the correct one
765 const result
: ChargingProfilesLimit
= {
766 limit
: lastButOneSchedule
!.limit
,
767 matchingChargingProfile
: chargingProfile
,
769 logger
.debug(debugLogMsg
, result
);
773 lastButOneSchedule
= schedulePeriod
;
774 // Handle the last schedule period
776 schedulePeriod
.startPeriod
===
777 chargingSchedule
.chargingSchedulePeriod
[
778 chargingSchedule
.chargingSchedulePeriod
.length
- 1
781 const result
: ChargingProfilesLimit
= {
782 limit
: lastButOneSchedule
.limit
,
783 matchingChargingProfile
: chargingProfile
,
785 logger
.debug(debugLogMsg
, result
);
795 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
797 * @param chargingProfile -
798 * @param currentDate -
801 const prepareRecurringChargingProfile
= (
802 chargingProfile
: ChargingProfile
,
806 const chargingSchedule
= chargingProfile
.chargingSchedule
;
807 let recurringInterval
: Interval
;
808 switch (chargingProfile
.recurrencyKind
) {
809 case RecurrencyKindType
.DAILY
:
810 recurringInterval
= {
811 start
: chargingSchedule
.startSchedule
!,
812 end
: addDays(chargingSchedule
.startSchedule
!, 1),
814 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
);
816 !isWithinInterval(currentDate
, recurringInterval
) &&
817 isBefore(chargingSchedule
.startSchedule
!, currentDate
)
819 chargingSchedule
.startSchedule
= addDays(
820 chargingSchedule
.startSchedule
!,
821 differenceInDays(chargingSchedule
.startSchedule
!, recurringInterval
.end
),
823 recurringInterval
= {
824 start
: chargingSchedule
.startSchedule
,
825 end
: addDays(chargingSchedule
.startSchedule
, 1),
829 case RecurrencyKindType
.WEEKLY
:
830 recurringInterval
= {
831 start
: chargingSchedule
.startSchedule
!,
832 end
: addWeeks(chargingSchedule
.startSchedule
!, 1),
834 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
);
836 !isWithinInterval(currentDate
, recurringInterval
) &&
837 isBefore(chargingSchedule
.startSchedule
!, currentDate
)
839 chargingSchedule
.startSchedule
= addWeeks(
840 chargingSchedule
.startSchedule
!,
841 differenceInWeeks(chargingSchedule
.startSchedule
!, recurringInterval
.end
),
843 recurringInterval
= {
844 start
: chargingSchedule
.startSchedule
,
845 end
: addWeeks(chargingSchedule
.startSchedule
, 1),
850 if (!isWithinInterval(currentDate
, recurringInterval
!)) {
852 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Recurring ${
853 chargingProfile.recurrencyKind
854 } charging profile id ${
855 chargingProfile.chargingProfileId
856 } startSchedule ${chargingSchedule.startSchedule!.toISOString()} is not properly translated to current recurrency time interval [${toDate(
857 recurringInterval!.start,
858 ).toISOString()}, ${toDate(recurringInterval!.end).toISOString()}]`,
863 const checkRecurringChargingProfileDuration
= (
864 chargingProfile
: ChargingProfile
,
869 chargingProfile
.chargingSchedule
.duration
! > differenceInSeconds(interval
.end
, interval
.start
)
872 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Recurring ${
873 chargingProfile.chargingProfileKind
874 } charging profile id ${chargingProfile.chargingProfileId} duration ${
875 chargingProfile.chargingSchedule.duration
876 } is greater than the recurrency time interval ${differenceInSeconds(
884 const getRandomSerialNumberSuffix
= (params
?: {
885 randomBytesLength
?: number;
888 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex');
889 if (params
?.upperCase
) {
890 return randomSerialNumberSuffix
.toUpperCase();
892 return randomSerialNumberSuffix
;