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
,
64 const moduleName
= 'ChargingStationUtils';
66 export const getChargingStationId
= (
68 stationTemplate
: ChargingStationTemplate
,
70 // In case of multiple instances: add instance index to charging station id
71 const instanceIndex
= process
.env
.CF_INSTANCE_INDEX
?? 0;
72 const idSuffix
= stationTemplate
?.nameSuffix
?? '';
73 const idStr
= `000000000${index.toString()}`;
74 return stationTemplate
?.fixedName
75 ? stationTemplate
.baseName
76 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
81 export const countReservableConnectors
= (connectors
: Map
<number, ConnectorStatus
>) => {
82 let reservableConnectors
= 0;
83 for (const [connectorId
, connectorStatus
] of connectors
) {
84 if (connectorId
=== 0) {
87 if (connectorStatus
.status === ConnectorStatusEnum
.Available
) {
88 ++reservableConnectors
;
91 return reservableConnectors
;
94 export const getHashId
= (index
: number, stationTemplate
: ChargingStationTemplate
): string => {
95 const chargingStationInfo
= {
96 chargePointModel
: stationTemplate
.chargePointModel
,
97 chargePointVendor
: stationTemplate
.chargePointVendor
,
98 ...(!isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
99 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
101 ...(!isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
102 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
104 ...(!isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
105 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
107 ...(!isUndefined(stationTemplate
.meterType
) && {
108 meterType
: stationTemplate
.meterType
,
111 return createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
112 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
116 export const checkChargingStation
= (
117 chargingStation
: ChargingStation
,
120 if (chargingStation
.started
=== false && chargingStation
.starting
=== false) {
121 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`);
127 export const getPhaseRotationValue
= (
129 numberOfPhases
: number,
130 ): string | undefined => {
132 if (connectorId
=== 0 && numberOfPhases
=== 0) {
133 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
134 } else if (connectorId
> 0 && numberOfPhases
=== 0) {
135 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
137 } else if (connectorId
> 0 && numberOfPhases
=== 1) {
138 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
139 } else if (connectorId
> 0 && numberOfPhases
=== 3) {
140 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
144 export const getMaxNumberOfEvses
= (evses
: Record
<string, EvseTemplate
>): number => {
148 return Object.keys(evses
).length
;
151 const getMaxNumberOfConnectors
= (connectors
: Record
<string, ConnectorStatus
>): number => {
155 return Object.keys(connectors
).length
;
158 export const getBootConnectorStatus
= (
159 chargingStation
: ChargingStation
,
161 connectorStatus
: ConnectorStatus
,
162 ): ConnectorStatusEnum
=> {
163 let connectorBootStatus
: ConnectorStatusEnum
;
165 !connectorStatus
?.status &&
166 (chargingStation
.isChargingStationAvailable() === false ||
167 chargingStation
.isConnectorAvailable(connectorId
) === false)
169 connectorBootStatus
= ConnectorStatusEnum
.Unavailable
;
170 } else if (!connectorStatus
?.status && connectorStatus
?.bootStatus
) {
171 // Set boot status in template at startup
172 connectorBootStatus
= connectorStatus
?.bootStatus
;
173 } else if (connectorStatus
?.status) {
174 // Set previous status at startup
175 connectorBootStatus
= connectorStatus
?.status;
177 // Set default status
178 connectorBootStatus
= ConnectorStatusEnum
.Available
;
180 return connectorBootStatus
;
183 export const checkTemplate
= (
184 stationTemplate
: ChargingStationTemplate
,
186 templateFile
: string,
188 if (isNullOrUndefined(stationTemplate
)) {
189 const errorMsg
= `Failed to read charging station template file ${templateFile}`;
190 logger
.error(`${logPrefix} ${errorMsg}`);
191 throw new BaseError(errorMsg
);
193 if (isEmptyObject(stationTemplate
)) {
194 const errorMsg
= `Empty charging station information from template file ${templateFile}`;
195 logger
.error(`${logPrefix} ${errorMsg}`);
196 throw new BaseError(errorMsg
);
198 if (isEmptyObject(stationTemplate
.AutomaticTransactionGenerator
!)) {
199 stationTemplate
.AutomaticTransactionGenerator
= Constants
.DEFAULT_ATG_CONFIGURATION
;
201 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
202 Constants
.DEFAULT_ATG_CONFIGURATION
,
205 if (isNullOrUndefined(stationTemplate
.idTagsFile
) || isEmptyString(stationTemplate
.idTagsFile
)) {
207 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`,
212 export const checkConnectorsConfiguration
= (
213 stationTemplate
: ChargingStationTemplate
,
215 templateFile
: string,
217 configuredMaxConnectors
: number;
218 templateMaxConnectors
: number;
219 templateMaxAvailableConnectors
: number;
221 const configuredMaxConnectors
= getConfiguredNumberOfConnectors(stationTemplate
);
222 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
);
223 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
!);
224 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
);
225 const templateMaxAvailableConnectors
= stationTemplate
.Connectors
![0]
226 ? templateMaxConnectors
- 1
227 : templateMaxConnectors
;
229 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
230 !stationTemplate
?.randomConnectors
233 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`,
235 stationTemplate
.randomConnectors
= true;
237 return { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
};
240 export const checkStationInfoConnectorStatus
= (
242 connectorStatus
: ConnectorStatus
,
244 templateFile
: string,
246 if (!isNullOrUndefined(connectorStatus
?.status)) {
248 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`,
250 delete connectorStatus
.status;
254 export const buildConnectorsMap
= (
255 connectors
: Record
<string, ConnectorStatus
>,
257 templateFile
: string,
258 ): Map
<number, ConnectorStatus
> => {
259 const connectorsMap
= new Map
<number, ConnectorStatus
>();
260 if (getMaxNumberOfConnectors(connectors
) > 0) {
261 for (const connector
in connectors
) {
262 const connectorStatus
= connectors
[connector
];
263 const connectorId
= convertToInt(connector
);
264 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
);
265 connectorsMap
.set(connectorId
, cloneObject
<ConnectorStatus
>(connectorStatus
));
269 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`,
272 return connectorsMap
;
275 export const initializeConnectorsMapStatus
= (
276 connectors
: Map
<number, ConnectorStatus
>,
279 for (const connectorId
of connectors
.keys()) {
280 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
282 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
287 if (connectorId
=== 0) {
288 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
;
289 if (isUndefined(connectors
.get(connectorId
)?.chargingProfiles
)) {
290 connectors
.get(connectorId
)!.chargingProfiles
= [];
294 isNullOrUndefined(connectors
.get(connectorId
)?.transactionStarted
)
296 initializeConnectorStatus(connectors
.get(connectorId
)!);
301 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
302 connectorStatus
.idTagLocalAuthorized
= false;
303 connectorStatus
.idTagAuthorized
= false;
304 connectorStatus
.transactionRemoteStarted
= false;
305 connectorStatus
.transactionStarted
= false;
306 delete connectorStatus
?.transactionStart
;
307 delete connectorStatus
?.transactionId
;
308 delete connectorStatus
?.localAuthorizeIdTag
;
309 delete connectorStatus
?.authorizeIdTag
;
310 delete connectorStatus
?.transactionIdTag
;
311 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0;
312 delete connectorStatus
?.transactionBeginMeterValue
;
315 export const createBootNotificationRequest
= (
316 stationInfo
: ChargingStationInfo
,
317 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
,
318 ): BootNotificationRequest
=> {
319 const ocppVersion
= stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
;
320 switch (ocppVersion
) {
321 case OCPPVersion
.VERSION_16
:
323 chargePointModel
: stationInfo
.chargePointModel
,
324 chargePointVendor
: stationInfo
.chargePointVendor
,
325 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
326 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
328 ...(!isUndefined(stationInfo
.chargePointSerialNumber
) && {
329 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
331 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
332 firmwareVersion
: stationInfo
.firmwareVersion
,
334 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
335 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
336 ...(!isUndefined(stationInfo
.meterSerialNumber
) && {
337 meterSerialNumber
: stationInfo
.meterSerialNumber
,
339 ...(!isUndefined(stationInfo
.meterType
) && {
340 meterType
: stationInfo
.meterType
,
342 } as OCPP16BootNotificationRequest
;
343 case OCPPVersion
.VERSION_20
:
344 case OCPPVersion
.VERSION_201
:
348 model
: stationInfo
.chargePointModel
,
349 vendorName
: stationInfo
.chargePointVendor
,
350 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
351 firmwareVersion
: stationInfo
.firmwareVersion
,
353 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
354 serialNumber
: stationInfo
.chargeBoxSerialNumber
,
356 ...((!isUndefined(stationInfo
.iccid
) || !isUndefined(stationInfo
.imsi
)) && {
358 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
359 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
363 } as OCPP20BootNotificationRequest
;
367 export const warnTemplateKeysDeprecation
= (
368 stationTemplate
: ChargingStationTemplate
,
370 templateFile
: string,
372 const templateKeys
: { deprecatedKey
: string; key
?: string }[] = [
373 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
374 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
375 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
377 for (const templateKey
of templateKeys
) {
378 warnDeprecatedTemplateKey(
380 templateKey
.deprecatedKey
,
383 !isUndefined(templateKey
.key
) ? `Use '${templateKey.key}' instead` : undefined,
385 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
);
389 export const stationTemplateToStationInfo
= (
390 stationTemplate
: ChargingStationTemplate
,
391 ): ChargingStationInfo
=> {
392 stationTemplate
= cloneObject
<ChargingStationTemplate
>(stationTemplate
);
393 delete stationTemplate
.power
;
394 delete stationTemplate
.powerUnit
;
395 delete stationTemplate
.Connectors
;
396 delete stationTemplate
.Evses
;
397 delete stationTemplate
.Configuration
;
398 delete stationTemplate
.AutomaticTransactionGenerator
;
399 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
400 delete stationTemplate
.chargePointSerialNumberPrefix
;
401 delete stationTemplate
.meterSerialNumberPrefix
;
402 return stationTemplate
as unknown
as ChargingStationInfo
;
405 export const createSerialNumber
= (
406 stationTemplate
: ChargingStationTemplate
,
407 stationInfo
: ChargingStationInfo
,
409 randomSerialNumberUpperCase
?: boolean;
410 randomSerialNumber
?: boolean;
412 randomSerialNumberUpperCase
: true,
413 randomSerialNumber
: true,
416 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
};
417 const serialNumberSuffix
= params
?.randomSerialNumber
418 ? getRandomSerialNumberSuffix({
419 upperCase
: params
.randomSerialNumberUpperCase
,
422 isNotEmptyString(stationTemplate
?.chargePointSerialNumberPrefix
) &&
423 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
424 isNotEmptyString(stationTemplate
?.chargeBoxSerialNumberPrefix
) &&
425 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
426 isNotEmptyString(stationTemplate
?.meterSerialNumberPrefix
) &&
427 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
430 export const propagateSerialNumber
= (
431 stationTemplate
: ChargingStationTemplate
,
432 stationInfoSrc
: ChargingStationInfo
,
433 stationInfoDst
: ChargingStationInfo
,
435 if (!stationInfoSrc
|| !stationTemplate
) {
437 'Missing charging station template or existing configuration to propagate serial number',
440 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
441 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
442 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
443 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
444 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
445 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
446 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
447 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
448 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
451 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
453 switch (stationInfo
.amperageLimitationUnit
) {
454 case AmpereUnits
.DECI_AMPERE
:
457 case AmpereUnits
.CENTI_AMPERE
:
460 case AmpereUnits
.MILLI_AMPERE
:
467 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
468 chargingStation
: ChargingStation
,
470 ): number | undefined => {
471 let limit
: number | undefined, matchingChargingProfile
: ChargingProfile
| undefined;
472 // Get charging profiles for connector id and sort by stack level
473 const chargingProfiles
=
474 cloneObject
<ChargingProfile
[]>(
475 chargingStation
.getConnectorStatus(connectorId
)!.chargingProfiles
!,
476 )?.sort((a
, b
) => b
.stackLevel
- a
.stackLevel
) ?? [];
477 // Get charging profiles on connector 0 and sort by stack level
478 if (isNotEmptyArray(chargingStation
.getConnectorStatus(0)?.chargingProfiles
)) {
479 chargingProfiles
.push(
480 ...cloneObject
<ChargingProfile
[]>(
481 chargingStation
.getConnectorStatus(0)!.chargingProfiles
!,
482 ).sort((a
, b
) => b
.stackLevel
- a
.stackLevel
),
485 if (isNotEmptyArray(chargingProfiles
)) {
486 const result
= getLimitFromChargingProfiles(
490 chargingStation
.logPrefix(),
492 if (!isNullOrUndefined(result
)) {
493 limit
= result
?.limit
;
494 matchingChargingProfile
= result
?.matchingChargingProfile
;
495 switch (chargingStation
.getCurrentOutType()) {
498 matchingChargingProfile
?.chargingSchedule
?.chargingRateUnit
===
499 ChargingRateUnitType
.WATT
501 : ACElectricUtils
.powerTotal(
502 chargingStation
.getNumberOfPhases(),
503 chargingStation
.getVoltageOut(),
509 matchingChargingProfile
?.chargingSchedule
?.chargingRateUnit
===
510 ChargingRateUnitType
.WATT
512 : DCElectricUtils
.power(chargingStation
.getVoltageOut(), limit
!);
514 const connectorMaximumPower
=
515 chargingStation
.getMaximumPower() / chargingStation
.powerDivider
;
516 if (limit
! > connectorMaximumPower
) {
518 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${matchingChargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
521 limit
= connectorMaximumPower
;
528 export const getDefaultVoltageOut
= (
529 currentType
: CurrentType
,
531 templateFile
: string,
533 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
534 let defaultVoltageOut
: number;
535 switch (currentType
) {
537 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
540 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
543 logger
.error(`${logPrefix} ${errorMsg}`);
544 throw new BaseError(errorMsg
);
546 return defaultVoltageOut
;
549 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
551 stationInfo
.idTagsFile
&&
552 join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
556 export const waitChargingStationEvents
= async (
557 emitter
: EventEmitter
,
558 event
: ChargingStationWorkerMessageEvents
,
559 eventsToWait
: number,
560 ): Promise
<number> => {
561 return new Promise
<number>((resolve
) => {
563 if (eventsToWait
=== 0) {
566 emitter
.on(event
, () => {
568 if (events
=== eventsToWait
) {
575 const getConfiguredNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
576 let configuredMaxConnectors
= 0;
577 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
) === true) {
578 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
579 configuredMaxConnectors
=
580 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)];
581 } else if (isUndefined(stationTemplate
.numberOfConnectors
) === false) {
582 configuredMaxConnectors
= stationTemplate
.numberOfConnectors
as number;
583 } else if (stationTemplate
.Connectors
&& !stationTemplate
.Evses
) {
584 configuredMaxConnectors
= stationTemplate
.Connectors
[0]
585 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
586 : getMaxNumberOfConnectors(stationTemplate
.Connectors
);
587 } else if (stationTemplate
.Evses
&& !stationTemplate
.Connectors
) {
588 for (const evse
in stationTemplate
.Evses
) {
592 configuredMaxConnectors
+= getMaxNumberOfConnectors(stationTemplate
.Evses
[evse
].Connectors
);
595 return configuredMaxConnectors
;
598 const checkConfiguredMaxConnectors
= (
599 configuredMaxConnectors
: number,
601 templateFile
: string,
603 if (configuredMaxConnectors
<= 0) {
605 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
610 const checkTemplateMaxConnectors
= (
611 templateMaxConnectors
: number,
613 templateFile
: string,
615 if (templateMaxConnectors
=== 0) {
617 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
619 } else if (templateMaxConnectors
< 0) {
621 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
626 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
627 connectorStatus
.availability
= AvailabilityType
.Operative
;
628 connectorStatus
.idTagLocalAuthorized
= false;
629 connectorStatus
.idTagAuthorized
= false;
630 connectorStatus
.transactionRemoteStarted
= false;
631 connectorStatus
.transactionStarted
= false;
632 connectorStatus
.energyActiveImportRegisterValue
= 0;
633 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0;
634 if (isUndefined(connectorStatus
.chargingProfiles
)) {
635 connectorStatus
.chargingProfiles
= [];
639 const warnDeprecatedTemplateKey
= (
640 template
: ChargingStationTemplate
,
643 templateFile
: string,
646 if (!isUndefined(template
[key
as keyof ChargingStationTemplate
])) {
647 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
648 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
650 logger
.warn(`${logPrefix} ${logMsg}`);
651 console
.warn(chalk
.yellow(`${logMsg}`));
655 const convertDeprecatedTemplateKey
= (
656 template
: ChargingStationTemplate
,
657 deprecatedKey
: string,
660 if (!isUndefined(template
[deprecatedKey
as keyof ChargingStationTemplate
])) {
661 if (!isUndefined(key
)) {
662 (template
as unknown
as Record
<string, unknown
>)[key
!] =
663 template
[deprecatedKey
as keyof ChargingStationTemplate
];
665 delete template
[deprecatedKey
as keyof ChargingStationTemplate
];
669 interface ChargingProfilesLimit
{
671 matchingChargingProfile
: ChargingProfile
;
675 * Charging profiles shall already be sorted by connector id and stack level (highest stack level has priority)
677 * @param chargingStation -
678 * @param connectorId -
679 * @param chargingProfiles -
681 * @returns ChargingProfilesLimit
683 const getLimitFromChargingProfiles
= (
684 chargingStation
: ChargingStation
,
686 chargingProfiles
: ChargingProfile
[],
688 ): ChargingProfilesLimit
| undefined => {
689 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
690 const currentDate
= new Date();
691 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
);
692 for (const chargingProfile
of chargingProfiles
) {
694 (isValidDate(chargingProfile
.validFrom
) &&
695 isBefore(currentDate
, chargingProfile
.validFrom
!)) ||
696 (isValidDate(chargingProfile
.validTo
) && isAfter(currentDate
, 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: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current 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: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date object. 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 if (isNullOrUndefined(chargingSchedule
?.startSchedule
)) {
738 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has (still) no startSchedule defined`,
742 if (isNullOrUndefined(chargingSchedule
?.duration
)) {
744 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined, not yet supported`,
748 // Check if the charging profile is active
750 isValidDate(chargingSchedule
?.startSchedule
) &&
751 isWithinInterval(currentDate
, {
752 start
: chargingSchedule
.startSchedule
!,
753 end
: addSeconds(chargingSchedule
.startSchedule
!, chargingSchedule
.duration
!),
756 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
757 const chargingSchedulePeriodCompareFn
= (
758 a
: ChargingSchedulePeriod
,
759 b
: ChargingSchedulePeriod
,
760 ) => a
.startPeriod
- b
.startPeriod
;
762 isArraySorted
<ChargingSchedulePeriod
>(
763 chargingSchedule
.chargingSchedulePeriod
,
764 chargingSchedulePeriodCompareFn
,
768 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
770 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
);
772 // Check if the first schedule period start period is equal to 0
773 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
775 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`,
779 // Handle only one schedule period
780 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
781 const result
: ChargingProfilesLimit
= {
782 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
783 matchingChargingProfile
: chargingProfile
,
785 logger
.debug(debugLogMsg
, result
);
788 let lastButOneSchedule
: ChargingSchedulePeriod
| undefined;
789 // Search for the right schedule period
790 for (const [index
, schedulePeriod
] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
791 // Find the right schedule period
794 addSeconds(chargingSchedule
.startSchedule
!, schedulePeriod
.startPeriod
),
798 // Found the schedule period: last but one is the correct one
799 const result
: ChargingProfilesLimit
= {
800 limit
: lastButOneSchedule
!.limit
,
801 matchingChargingProfile
: chargingProfile
,
803 logger
.debug(debugLogMsg
, result
);
807 lastButOneSchedule
= schedulePeriod
;
808 // Handle the last schedule period within the charging profile duration
810 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
811 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
812 chargingSchedule
.duration
! >
815 chargingSchedule
.startSchedule
!,
816 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
,
818 chargingSchedule
.startSchedule
!,
821 const result
: ChargingProfilesLimit
= {
822 limit
: lastButOneSchedule
.limit
,
823 matchingChargingProfile
: chargingProfile
,
825 logger
.debug(debugLogMsg
, result
);
835 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
837 * @param chargingProfile -
838 * @param currentDate -
841 const prepareRecurringChargingProfile
= (
842 chargingProfile
: ChargingProfile
,
846 const chargingSchedule
= chargingProfile
.chargingSchedule
;
847 let recurringIntervalTranslated
= false;
848 let recurringInterval
: Interval
;
849 switch (chargingProfile
.recurrencyKind
) {
850 case RecurrencyKindType
.DAILY
:
851 recurringInterval
= {
852 start
: chargingSchedule
.startSchedule
!,
853 end
: addDays(chargingSchedule
.startSchedule
!, 1),
855 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
);
857 !isWithinInterval(currentDate
, recurringInterval
) &&
858 isBefore(recurringInterval
.end
, currentDate
)
860 chargingSchedule
.startSchedule
= addDays(
861 recurringInterval
.start
,
862 differenceInDays(currentDate
, recurringInterval
.start
),
864 recurringInterval
= {
865 start
: chargingSchedule
.startSchedule
,
866 end
: addDays(chargingSchedule
.startSchedule
, 1),
868 recurringIntervalTranslated
= true;
871 case RecurrencyKindType
.WEEKLY
:
872 recurringInterval
= {
873 start
: chargingSchedule
.startSchedule
!,
874 end
: addWeeks(chargingSchedule
.startSchedule
!, 1),
876 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
);
878 !isWithinInterval(currentDate
, recurringInterval
) &&
879 isBefore(recurringInterval
.end
, currentDate
)
881 chargingSchedule
.startSchedule
= addWeeks(
882 recurringInterval
.start
,
883 differenceInWeeks(currentDate
, recurringInterval
.start
),
885 recurringInterval
= {
886 start
: chargingSchedule
.startSchedule
,
887 end
: addWeeks(chargingSchedule
.startSchedule
, 1),
889 recurringIntervalTranslated
= true;
894 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} recurrency kind ${chargingProfile.recurrencyKind} is not supported`,
897 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
899 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
900 chargingProfile.recurrencyKind
901 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
902 recurringInterval!.start,
903 ).toISOString()}, ${toDate(
904 recurringInterval!.end,
905 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
908 return recurringIntervalTranslated
;
911 const checkRecurringChargingProfileDuration
= (
912 chargingProfile
: ChargingProfile
,
916 if (isNullOrUndefined(chargingProfile
.chargingSchedule
.duration
)) {
918 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
919 chargingProfile.chargingProfileKind
920 } charging profile id ${
921 chargingProfile.chargingProfileId
922 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
927 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
);
929 chargingProfile
.chargingSchedule
.duration
! > differenceInSeconds(interval
.end
, interval
.start
)
932 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
933 chargingProfile.chargingProfileKind
934 } charging profile id ${chargingProfile.chargingProfileId} duration ${
935 chargingProfile.chargingSchedule.duration
936 } is greater than the recurrency time interval duration ${differenceInSeconds(
941 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
);
945 const getRandomSerialNumberSuffix
= (params
?: {
946 randomBytesLength
?: number;
949 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex');
950 if (params
?.upperCase
) {
951 return randomSerialNumberSuffix
.toUpperCase();
953 return randomSerialNumberSuffix
;