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';
19 import type { ChargingStation
} from
'./ChargingStation';
20 import { BaseError
} from
'../exception';
24 type BootNotificationRequest
,
27 ChargingProfileKindType
,
29 type ChargingSchedulePeriod
,
30 type ChargingStationInfo
,
31 type ChargingStationTemplate
,
32 ChargingStationWorkerMessageEvents
,
33 ConnectorPhaseRotation
,
38 type OCPP16BootNotificationRequest
,
39 type OCPP20BootNotificationRequest
,
62 const moduleName
= 'ChargingStationUtils';
64 export const getChargingStationId
= (
66 stationTemplate
: ChargingStationTemplate
,
68 // In case of multiple instances: add instance index to charging station id
69 const instanceIndex
= process
.env
.CF_INSTANCE_INDEX
?? 0;
70 const idSuffix
= stationTemplate
?.nameSuffix
?? '';
71 const idStr
= `000000000${index.toString()}`;
72 return stationTemplate
?.fixedName
73 ? stationTemplate
.baseName
74 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
79 export const countReservableConnectors
= (connectors
: Map
<number, ConnectorStatus
>) => {
80 let reservableConnectors
= 0;
81 for (const [connectorId
, connectorStatus
] of connectors
) {
82 if (connectorId
=== 0) {
85 if (connectorStatus
.status === ConnectorStatusEnum
.Available
) {
86 ++reservableConnectors
;
89 return reservableConnectors
;
92 export const getHashId
= (index
: number, stationTemplate
: ChargingStationTemplate
): string => {
93 const chargingStationInfo
= {
94 chargePointModel
: stationTemplate
.chargePointModel
,
95 chargePointVendor
: stationTemplate
.chargePointVendor
,
96 ...(!isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
97 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
99 ...(!isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
100 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
102 ...(!isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
103 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
105 ...(!isUndefined(stationTemplate
.meterType
) && {
106 meterType
: stationTemplate
.meterType
,
109 return createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
110 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
114 export const checkChargingStation
= (
115 chargingStation
: ChargingStation
,
118 if (chargingStation
.started
=== false && chargingStation
.starting
=== false) {
119 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`);
125 export const getPhaseRotationValue
= (
127 numberOfPhases
: number,
128 ): string | undefined => {
130 if (connectorId
=== 0 && numberOfPhases
=== 0) {
131 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
132 } else if (connectorId
> 0 && numberOfPhases
=== 0) {
133 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
135 } else if (connectorId
> 0 && numberOfPhases
=== 1) {
136 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
137 } else if (connectorId
> 0 && numberOfPhases
=== 3) {
138 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
142 export const getMaxNumberOfEvses
= (evses
: Record
<string, EvseTemplate
>): number => {
146 return Object.keys(evses
).length
;
149 const getMaxNumberOfConnectors
= (connectors
: Record
<string, ConnectorStatus
>): number => {
153 return Object.keys(connectors
).length
;
156 export const getBootConnectorStatus
= (
157 chargingStation
: ChargingStation
,
159 connectorStatus
: ConnectorStatus
,
160 ): ConnectorStatusEnum
=> {
161 let connectorBootStatus
: ConnectorStatusEnum
;
163 !connectorStatus
?.status &&
164 (chargingStation
.isChargingStationAvailable() === false ||
165 chargingStation
.isConnectorAvailable(connectorId
) === false)
167 connectorBootStatus
= ConnectorStatusEnum
.Unavailable
;
168 } else if (!connectorStatus
?.status && connectorStatus
?.bootStatus
) {
169 // Set boot status in template at startup
170 connectorBootStatus
= connectorStatus
?.bootStatus
;
171 } else if (connectorStatus
?.status) {
172 // Set previous status at startup
173 connectorBootStatus
= connectorStatus
?.status;
175 // Set default status
176 connectorBootStatus
= ConnectorStatusEnum
.Available
;
178 return connectorBootStatus
;
181 export const checkTemplate
= (
182 stationTemplate
: ChargingStationTemplate
,
184 templateFile
: string,
186 if (isNullOrUndefined(stationTemplate
)) {
187 const errorMsg
= `Failed to read charging station template file ${templateFile}`;
188 logger
.error(`${logPrefix} ${errorMsg}`);
189 throw new BaseError(errorMsg
);
191 if (isEmptyObject(stationTemplate
)) {
192 const errorMsg
= `Empty charging station information from template file ${templateFile}`;
193 logger
.error(`${logPrefix} ${errorMsg}`);
194 throw new BaseError(errorMsg
);
196 if (isEmptyObject(stationTemplate
.AutomaticTransactionGenerator
!)) {
197 stationTemplate
.AutomaticTransactionGenerator
= Constants
.DEFAULT_ATG_CONFIGURATION
;
199 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
200 Constants
.DEFAULT_ATG_CONFIGURATION
,
203 if (isNullOrUndefined(stationTemplate
.idTagsFile
) || isEmptyString(stationTemplate
.idTagsFile
)) {
205 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`,
210 export const checkConnectorsConfiguration
= (
211 stationTemplate
: ChargingStationTemplate
,
213 templateFile
: string,
215 configuredMaxConnectors
: number;
216 templateMaxConnectors
: number;
217 templateMaxAvailableConnectors
: number;
219 const configuredMaxConnectors
= getConfiguredNumberOfConnectors(stationTemplate
);
220 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
);
221 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
!);
222 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
);
223 const templateMaxAvailableConnectors
= stationTemplate
.Connectors
![0]
224 ? templateMaxConnectors
- 1
225 : templateMaxConnectors
;
227 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
228 !stationTemplate
?.randomConnectors
231 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`,
233 stationTemplate
.randomConnectors
= true;
235 return { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
};
238 export const checkStationInfoConnectorStatus
= (
240 connectorStatus
: ConnectorStatus
,
242 templateFile
: string,
244 if (!isNullOrUndefined(connectorStatus
?.status)) {
246 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`,
248 delete connectorStatus
.status;
252 export const buildConnectorsMap
= (
253 connectors
: Record
<string, ConnectorStatus
>,
255 templateFile
: string,
256 ): Map
<number, ConnectorStatus
> => {
257 const connectorsMap
= new Map
<number, ConnectorStatus
>();
258 if (getMaxNumberOfConnectors(connectors
) > 0) {
259 for (const connector
in connectors
) {
260 const connectorStatus
= connectors
[connector
];
261 const connectorId
= convertToInt(connector
);
262 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
);
263 connectorsMap
.set(connectorId
, cloneObject
<ConnectorStatus
>(connectorStatus
));
267 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`,
270 return connectorsMap
;
273 export const initializeConnectorsMapStatus
= (
274 connectors
: Map
<number, ConnectorStatus
>,
277 for (const connectorId
of connectors
.keys()) {
278 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
280 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
285 if (connectorId
=== 0) {
286 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
;
287 if (isUndefined(connectors
.get(connectorId
)?.chargingProfiles
)) {
288 connectors
.get(connectorId
)!.chargingProfiles
= [];
292 isNullOrUndefined(connectors
.get(connectorId
)?.transactionStarted
)
294 initializeConnectorStatus(connectors
.get(connectorId
)!);
299 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
300 connectorStatus
.idTagLocalAuthorized
= false;
301 connectorStatus
.idTagAuthorized
= false;
302 connectorStatus
.transactionRemoteStarted
= false;
303 connectorStatus
.transactionStarted
= false;
304 delete connectorStatus
?.transactionStart
;
305 delete connectorStatus
?.transactionId
;
306 delete connectorStatus
?.localAuthorizeIdTag
;
307 delete connectorStatus
?.authorizeIdTag
;
308 delete connectorStatus
?.transactionIdTag
;
309 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0;
310 delete connectorStatus
?.transactionBeginMeterValue
;
313 export const createBootNotificationRequest
= (
314 stationInfo
: ChargingStationInfo
,
315 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
,
316 ): BootNotificationRequest
=> {
317 const ocppVersion
= stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
;
318 switch (ocppVersion
) {
319 case OCPPVersion
.VERSION_16
:
321 chargePointModel
: stationInfo
.chargePointModel
,
322 chargePointVendor
: stationInfo
.chargePointVendor
,
323 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
324 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
326 ...(!isUndefined(stationInfo
.chargePointSerialNumber
) && {
327 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
329 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
330 firmwareVersion
: stationInfo
.firmwareVersion
,
332 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
333 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
334 ...(!isUndefined(stationInfo
.meterSerialNumber
) && {
335 meterSerialNumber
: stationInfo
.meterSerialNumber
,
337 ...(!isUndefined(stationInfo
.meterType
) && {
338 meterType
: stationInfo
.meterType
,
340 } as OCPP16BootNotificationRequest
;
341 case OCPPVersion
.VERSION_20
:
342 case OCPPVersion
.VERSION_201
:
346 model
: stationInfo
.chargePointModel
,
347 vendorName
: stationInfo
.chargePointVendor
,
348 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
349 firmwareVersion
: stationInfo
.firmwareVersion
,
351 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
352 serialNumber
: stationInfo
.chargeBoxSerialNumber
,
354 ...((!isUndefined(stationInfo
.iccid
) || !isUndefined(stationInfo
.imsi
)) && {
356 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
357 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
361 } as OCPP20BootNotificationRequest
;
365 export const warnTemplateKeysDeprecation
= (
366 stationTemplate
: ChargingStationTemplate
,
368 templateFile
: string,
370 const templateKeys
: { deprecatedKey
: string; key
?: string }[] = [
371 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
372 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
373 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
375 for (const templateKey
of templateKeys
) {
376 warnDeprecatedTemplateKey(
378 templateKey
.deprecatedKey
,
381 !isUndefined(templateKey
.key
) ? `Use '${templateKey.key}' instead` : undefined,
383 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
);
387 export const stationTemplateToStationInfo
= (
388 stationTemplate
: ChargingStationTemplate
,
389 ): ChargingStationInfo
=> {
390 stationTemplate
= cloneObject
<ChargingStationTemplate
>(stationTemplate
);
391 delete stationTemplate
.power
;
392 delete stationTemplate
.powerUnit
;
393 delete stationTemplate
.Connectors
;
394 delete stationTemplate
.Evses
;
395 delete stationTemplate
.Configuration
;
396 delete stationTemplate
.AutomaticTransactionGenerator
;
397 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
398 delete stationTemplate
.chargePointSerialNumberPrefix
;
399 delete stationTemplate
.meterSerialNumberPrefix
;
400 return stationTemplate
as unknown
as ChargingStationInfo
;
403 export const createSerialNumber
= (
404 stationTemplate
: ChargingStationTemplate
,
405 stationInfo
: ChargingStationInfo
,
407 randomSerialNumberUpperCase
?: boolean;
408 randomSerialNumber
?: boolean;
410 randomSerialNumberUpperCase
: true,
411 randomSerialNumber
: true,
414 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
};
415 const serialNumberSuffix
= params
?.randomSerialNumber
416 ? getRandomSerialNumberSuffix({
417 upperCase
: params
.randomSerialNumberUpperCase
,
420 isNotEmptyString(stationTemplate
?.chargePointSerialNumberPrefix
) &&
421 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
422 isNotEmptyString(stationTemplate
?.chargeBoxSerialNumberPrefix
) &&
423 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
424 isNotEmptyString(stationTemplate
?.meterSerialNumberPrefix
) &&
425 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
428 export const propagateSerialNumber
= (
429 stationTemplate
: ChargingStationTemplate
,
430 stationInfoSrc
: ChargingStationInfo
,
431 stationInfoDst
: ChargingStationInfo
,
433 if (!stationInfoSrc
|| !stationTemplate
) {
435 'Missing charging station template or existing configuration to propagate serial number',
438 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
439 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
440 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
441 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
442 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
443 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
444 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
445 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
446 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
449 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
451 switch (stationInfo
.amperageLimitationUnit
) {
452 case AmpereUnits
.DECI_AMPERE
:
455 case AmpereUnits
.CENTI_AMPERE
:
458 case AmpereUnits
.MILLI_AMPERE
:
465 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
466 chargingStation
: ChargingStation
,
468 ): number | undefined => {
469 let limit
: number | undefined, matchingChargingProfile
: ChargingProfile
| undefined;
470 // Get charging profiles for connector id and sort by stack level
471 const chargingProfiles
=
472 cloneObject
<ChargingProfile
[]>(
473 chargingStation
.getConnectorStatus(connectorId
)!.chargingProfiles
!,
474 )?.sort((a
, b
) => b
.stackLevel
- a
.stackLevel
) ?? [];
475 // Get charging profiles on connector 0 and sort by stack level
476 if (chargingStation
.getConnectorStatus(0)?.chargingProfiles
) {
477 chargingProfiles
.push(
478 ...cloneObject
<ChargingProfile
[]>(
479 chargingStation
.getConnectorStatus(0)!.chargingProfiles
!,
480 ).sort((a
, b
) => b
.stackLevel
- a
.stackLevel
),
483 if (isNotEmptyArray(chargingProfiles
)) {
484 const result
= getLimitFromChargingProfiles(
488 chargingStation
.logPrefix(),
490 if (!isNullOrUndefined(result
)) {
491 limit
= result
?.limit
;
492 matchingChargingProfile
= result
?.matchingChargingProfile
;
493 switch (chargingStation
.getCurrentOutType()) {
496 matchingChargingProfile
?.chargingSchedule
?.chargingRateUnit
===
497 ChargingRateUnitType
.WATT
499 : ACElectricUtils
.powerTotal(
500 chargingStation
.getNumberOfPhases(),
501 chargingStation
.getVoltageOut(),
507 matchingChargingProfile
?.chargingSchedule
?.chargingRateUnit
===
508 ChargingRateUnitType
.WATT
510 : DCElectricUtils
.power(chargingStation
.getVoltageOut(), limit
!);
512 const connectorMaximumPower
=
513 chargingStation
.getMaximumPower() / chargingStation
.powerDivider
;
514 if (limit
! > connectorMaximumPower
) {
516 `${chargingStation.logPrefix()} Charging profile id ${matchingChargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
519 limit
= connectorMaximumPower
;
526 export const getDefaultVoltageOut
= (
527 currentType
: CurrentType
,
529 templateFile
: string,
531 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
532 let defaultVoltageOut
: number;
533 switch (currentType
) {
535 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
538 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
541 logger
.error(`${logPrefix} ${errorMsg}`);
542 throw new BaseError(errorMsg
);
544 return defaultVoltageOut
;
547 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
549 stationInfo
.idTagsFile
&&
550 join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
554 export const waitChargingStationEvents
= async (
555 emitter
: EventEmitter
,
556 event
: ChargingStationWorkerMessageEvents
,
557 eventsToWait
: number,
558 ): Promise
<number> => {
559 return new Promise
<number>((resolve
) => {
561 if (eventsToWait
=== 0) {
564 emitter
.on(event
, () => {
566 if (events
=== eventsToWait
) {
573 const getConfiguredNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
574 let configuredMaxConnectors
= 0;
575 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
) === true) {
576 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
577 configuredMaxConnectors
=
578 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)];
579 } else if (isUndefined(stationTemplate
.numberOfConnectors
) === false) {
580 configuredMaxConnectors
= stationTemplate
.numberOfConnectors
as number;
581 } else if (stationTemplate
.Connectors
&& !stationTemplate
.Evses
) {
582 configuredMaxConnectors
= stationTemplate
.Connectors
[0]
583 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
584 : getMaxNumberOfConnectors(stationTemplate
.Connectors
);
585 } else if (stationTemplate
.Evses
&& !stationTemplate
.Connectors
) {
586 for (const evse
in stationTemplate
.Evses
) {
590 configuredMaxConnectors
+= getMaxNumberOfConnectors(stationTemplate
.Evses
[evse
].Connectors
);
593 return configuredMaxConnectors
;
596 const checkConfiguredMaxConnectors
= (
597 configuredMaxConnectors
: number,
599 templateFile
: string,
601 if (configuredMaxConnectors
<= 0) {
603 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
608 const checkTemplateMaxConnectors
= (
609 templateMaxConnectors
: number,
611 templateFile
: string,
613 if (templateMaxConnectors
=== 0) {
615 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
617 } else if (templateMaxConnectors
< 0) {
619 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
624 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
625 connectorStatus
.availability
= AvailabilityType
.Operative
;
626 connectorStatus
.idTagLocalAuthorized
= false;
627 connectorStatus
.idTagAuthorized
= false;
628 connectorStatus
.transactionRemoteStarted
= false;
629 connectorStatus
.transactionStarted
= false;
630 connectorStatus
.energyActiveImportRegisterValue
= 0;
631 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0;
632 if (isUndefined(connectorStatus
.chargingProfiles
)) {
633 connectorStatus
.chargingProfiles
= [];
637 const warnDeprecatedTemplateKey
= (
638 template
: ChargingStationTemplate
,
641 templateFile
: string,
644 if (!isUndefined(template
[key
as keyof ChargingStationTemplate
])) {
645 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
646 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
648 logger
.warn(`${logPrefix} ${logMsg}`);
649 console
.warn(chalk
.yellow(`${logMsg}`));
653 const convertDeprecatedTemplateKey
= (
654 template
: ChargingStationTemplate
,
655 deprecatedKey
: string,
658 if (!isUndefined(template
[deprecatedKey
as keyof ChargingStationTemplate
])) {
659 if (!isUndefined(key
)) {
660 (template
as unknown
as Record
<string, unknown
>)[key
!] =
661 template
[deprecatedKey
as keyof ChargingStationTemplate
];
663 delete template
[deprecatedKey
as keyof ChargingStationTemplate
];
667 interface ChargingProfilesLimit
{
669 matchingChargingProfile
: ChargingProfile
;
673 * Charging profiles should already be sorted by connector id and stack level (highest stack level has priority)
675 * @param chargingProfiles -
677 * @returns ChargingProfilesLimit
679 const getLimitFromChargingProfiles
= (
680 chargingStation
: ChargingStation
,
682 chargingProfiles
: ChargingProfile
[],
684 ): ChargingProfilesLimit
| undefined => {
685 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
686 const currentDate
= new Date();
687 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
);
688 for (const chargingProfile
of chargingProfiles
) {
690 isValidDate(chargingProfile
.validFrom
) &&
691 isValidDate(chargingProfile
.validTo
) &&
692 !isWithinInterval(currentDate
, {
693 start
: chargingProfile
.validFrom
!,
694 end
: chargingProfile
.validTo
!,
698 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${
699 chargingProfile.chargingProfileId
700 } is not valid for the current date ${currentDate.toISOString()}`,
704 const chargingSchedule
= chargingProfile
.chargingSchedule
;
705 if (connectorStatus
?.transactionStarted
&& !chargingSchedule
?.startSchedule
) {
707 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}. Trying to set it to the connector transaction start date`,
709 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
710 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
;
712 if (!(chargingSchedule
?.startSchedule
instanceof Date)) {
714 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not a Date object in charging profile id ${chargingProfile.chargingProfileId}. Trying to convert it to a Date object`,
716 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!;
719 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
720 isNullOrUndefined(chargingProfile
.recurrencyKind
)
723 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
727 if (chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
) {
728 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
);
730 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RELATIVE
&&
731 connectorStatus
?.transactionStarted
733 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
;
735 // Check if the charging profile is active
737 isValidDate(chargingSchedule
.startSchedule
) &&
738 isAfter(addSeconds(chargingSchedule
.startSchedule
!, chargingSchedule
.duration
!), currentDate
)
740 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
741 // Handling of only one schedule period
743 chargingSchedule
.chargingSchedulePeriod
.length
=== 1 &&
744 chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
=== 0
746 const result
: ChargingProfilesLimit
= {
747 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
748 matchingChargingProfile
: chargingProfile
,
750 logger
.debug(debugLogMsg
, result
);
753 let lastButOneSchedule
: ChargingSchedulePeriod
| undefined;
754 // Search for the right schedule period
755 for (const schedulePeriod
of chargingSchedule
.chargingSchedulePeriod
) {
756 // Find the right schedule period
759 addSeconds(chargingSchedule
.startSchedule
!, schedulePeriod
.startPeriod
),
763 // Found the schedule period: last but one is the correct one
764 const result
: ChargingProfilesLimit
= {
765 limit
: lastButOneSchedule
!.limit
,
766 matchingChargingProfile
: chargingProfile
,
768 logger
.debug(debugLogMsg
, result
);
772 lastButOneSchedule
= schedulePeriod
;
773 // Handle the last schedule period
775 schedulePeriod
.startPeriod
===
776 chargingSchedule
.chargingSchedulePeriod
[
777 chargingSchedule
.chargingSchedulePeriod
.length
- 1
780 const result
: ChargingProfilesLimit
= {
781 limit
: lastButOneSchedule
.limit
,
782 matchingChargingProfile
: chargingProfile
,
784 logger
.debug(debugLogMsg
, result
);
794 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
796 * @param chargingProfile -
797 * @param currentDate -
800 const prepareRecurringChargingProfile
= (
801 chargingProfile
: ChargingProfile
,
805 const chargingSchedule
= chargingProfile
.chargingSchedule
;
806 let recurringInterval
: Interval
;
807 switch (chargingProfile
.recurrencyKind
) {
808 case RecurrencyKindType
.DAILY
:
809 recurringInterval
= {
810 start
: chargingSchedule
.startSchedule
!,
811 end
: addDays(chargingSchedule
.startSchedule
!, 1),
814 !isWithinInterval(currentDate
, recurringInterval
) &&
815 isBefore(chargingSchedule
.startSchedule
!, currentDate
)
817 chargingSchedule
.startSchedule
= addDays(
818 chargingSchedule
.startSchedule
!,
819 differenceInDays(chargingSchedule
.startSchedule
!, recurringInterval
.end
),
821 recurringInterval
= {
822 start
: chargingSchedule
.startSchedule
,
823 end
: addDays(chargingSchedule
.startSchedule
, 1),
827 case RecurrencyKindType
.WEEKLY
:
828 recurringInterval
= {
829 start
: chargingSchedule
.startSchedule
!,
830 end
: addWeeks(chargingSchedule
.startSchedule
!, 1),
833 !isWithinInterval(currentDate
, recurringInterval
) &&
834 isBefore(chargingSchedule
.startSchedule
!, currentDate
)
836 chargingSchedule
.startSchedule
= addWeeks(
837 chargingSchedule
.startSchedule
!,
838 differenceInWeeks(chargingSchedule
.startSchedule
!, recurringInterval
.end
),
840 recurringInterval
= {
841 start
: chargingSchedule
.startSchedule
,
842 end
: addWeeks(chargingSchedule
.startSchedule
, 1),
847 if (!isWithinInterval(currentDate
, recurringInterval
!)) {
849 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Recurring ${
850 chargingProfile.recurrencyKind
851 } charging profile id ${
852 chargingProfile.chargingProfileId
853 } startSchedule ${chargingSchedule.startSchedule!.toISOString()} is not properly translated to current recurrency time interval [${toDate(
854 recurringInterval!.start,
855 ).toISOString()}, ${toDate(recurringInterval!.end).toISOString()}]`,
860 const getRandomSerialNumberSuffix
= (params
?: {
861 randomBytesLength
?: number;
864 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex');
865 if (params
?.upperCase
) {
866 return randomSerialNumberSuffix
.toUpperCase();
868 return randomSerialNumberSuffix
;