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';
21 import type { ChargingStation
} from
'./ChargingStation';
22 import { BaseError
} from
'../exception';
26 type BootNotificationRequest
,
29 ChargingProfileKindType
,
31 type ChargingSchedulePeriod
,
32 type ChargingStationInfo
,
33 type ChargingStationTemplate
,
34 ChargingStationWorkerMessageEvents
,
35 ConnectorPhaseRotation
,
40 type OCPP16BootNotificationRequest
,
41 type OCPP20BootNotificationRequest
,
65 const moduleName
= 'ChargingStationUtils';
67 export const getChargingStationId
= (
69 stationTemplate
: ChargingStationTemplate
,
71 // In case of multiple instances: add instance index to charging station id
72 const instanceIndex
= process
.env
.CF_INSTANCE_INDEX
?? 0;
73 const idSuffix
= stationTemplate
?.nameSuffix
?? '';
74 const idStr
= `000000000${index.toString()}`;
75 return stationTemplate
?.fixedName
76 ? stationTemplate
.baseName
77 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
82 export const countReservableConnectors
= (connectors
: Map
<number, ConnectorStatus
>) => {
83 let reservableConnectors
= 0;
84 for (const [connectorId
, connectorStatus
] of connectors
) {
85 if (connectorId
=== 0) {
88 if (connectorStatus
.status === ConnectorStatusEnum
.Available
) {
89 ++reservableConnectors
;
92 return reservableConnectors
;
95 export const getHashId
= (index
: number, stationTemplate
: ChargingStationTemplate
): string => {
96 const chargingStationInfo
= {
97 chargePointModel
: stationTemplate
.chargePointModel
,
98 chargePointVendor
: stationTemplate
.chargePointVendor
,
99 ...(!isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
100 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
102 ...(!isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
103 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
105 ...(!isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
106 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
108 ...(!isUndefined(stationTemplate
.meterType
) && {
109 meterType
: stationTemplate
.meterType
,
112 return createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
113 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
117 export const checkChargingStation
= (
118 chargingStation
: ChargingStation
,
121 if (chargingStation
.started
=== false && chargingStation
.starting
=== false) {
122 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`);
128 export const getPhaseRotationValue
= (
130 numberOfPhases
: number,
131 ): string | undefined => {
133 if (connectorId
=== 0 && numberOfPhases
=== 0) {
134 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
135 } else if (connectorId
> 0 && numberOfPhases
=== 0) {
136 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
138 } else if (connectorId
> 0 && numberOfPhases
=== 1) {
139 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
140 } else if (connectorId
> 0 && numberOfPhases
=== 3) {
141 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
145 export const getMaxNumberOfEvses
= (evses
: Record
<string, EvseTemplate
>): number => {
149 return Object.keys(evses
).length
;
152 const getMaxNumberOfConnectors
= (connectors
: Record
<string, ConnectorStatus
>): number => {
156 return Object.keys(connectors
).length
;
159 export const getBootConnectorStatus
= (
160 chargingStation
: ChargingStation
,
162 connectorStatus
: ConnectorStatus
,
163 ): ConnectorStatusEnum
=> {
164 let connectorBootStatus
: ConnectorStatusEnum
;
166 !connectorStatus
?.status &&
167 (chargingStation
.isChargingStationAvailable() === false ||
168 chargingStation
.isConnectorAvailable(connectorId
) === false)
170 connectorBootStatus
= ConnectorStatusEnum
.Unavailable
;
171 } else if (!connectorStatus
?.status && connectorStatus
?.bootStatus
) {
172 // Set boot status in template at startup
173 connectorBootStatus
= connectorStatus
?.bootStatus
;
174 } else if (connectorStatus
?.status) {
175 // Set previous status at startup
176 connectorBootStatus
= connectorStatus
?.status;
178 // Set default status
179 connectorBootStatus
= ConnectorStatusEnum
.Available
;
181 return connectorBootStatus
;
184 export const checkTemplate
= (
185 stationTemplate
: ChargingStationTemplate
,
187 templateFile
: string,
189 if (isNullOrUndefined(stationTemplate
)) {
190 const errorMsg
= `Failed to read charging station template file ${templateFile}`;
191 logger
.error(`${logPrefix} ${errorMsg}`);
192 throw new BaseError(errorMsg
);
194 if (isEmptyObject(stationTemplate
)) {
195 const errorMsg
= `Empty charging station information from template file ${templateFile}`;
196 logger
.error(`${logPrefix} ${errorMsg}`);
197 throw new BaseError(errorMsg
);
199 if (isEmptyObject(stationTemplate
.AutomaticTransactionGenerator
!)) {
200 stationTemplate
.AutomaticTransactionGenerator
= Constants
.DEFAULT_ATG_CONFIGURATION
;
202 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
203 Constants
.DEFAULT_ATG_CONFIGURATION
,
206 if (isNullOrUndefined(stationTemplate
.idTagsFile
) || isEmptyString(stationTemplate
.idTagsFile
)) {
208 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`,
213 export const checkConnectorsConfiguration
= (
214 stationTemplate
: ChargingStationTemplate
,
216 templateFile
: string,
218 configuredMaxConnectors
: number;
219 templateMaxConnectors
: number;
220 templateMaxAvailableConnectors
: number;
222 const configuredMaxConnectors
= getConfiguredNumberOfConnectors(stationTemplate
);
223 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
);
224 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
!);
225 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
);
226 const templateMaxAvailableConnectors
= stationTemplate
.Connectors
![0]
227 ? templateMaxConnectors
- 1
228 : templateMaxConnectors
;
230 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
231 !stationTemplate
?.randomConnectors
234 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`,
236 stationTemplate
.randomConnectors
= true;
238 return { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
};
241 export const checkStationInfoConnectorStatus
= (
243 connectorStatus
: ConnectorStatus
,
245 templateFile
: string,
247 if (!isNullOrUndefined(connectorStatus
?.status)) {
249 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`,
251 delete connectorStatus
.status;
255 export const buildConnectorsMap
= (
256 connectors
: Record
<string, ConnectorStatus
>,
258 templateFile
: string,
259 ): Map
<number, ConnectorStatus
> => {
260 const connectorsMap
= new Map
<number, ConnectorStatus
>();
261 if (getMaxNumberOfConnectors(connectors
) > 0) {
262 for (const connector
in connectors
) {
263 const connectorStatus
= connectors
[connector
];
264 const connectorId
= convertToInt(connector
);
265 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
);
266 connectorsMap
.set(connectorId
, cloneObject
<ConnectorStatus
>(connectorStatus
));
270 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`,
273 return connectorsMap
;
276 export const initializeConnectorsMapStatus
= (
277 connectors
: Map
<number, ConnectorStatus
>,
280 for (const connectorId
of connectors
.keys()) {
281 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
283 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
288 if (connectorId
=== 0) {
289 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
;
290 if (isUndefined(connectors
.get(connectorId
)?.chargingProfiles
)) {
291 connectors
.get(connectorId
)!.chargingProfiles
= [];
295 isNullOrUndefined(connectors
.get(connectorId
)?.transactionStarted
)
297 initializeConnectorStatus(connectors
.get(connectorId
)!);
302 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
303 connectorStatus
.idTagLocalAuthorized
= false;
304 connectorStatus
.idTagAuthorized
= false;
305 connectorStatus
.transactionRemoteStarted
= false;
306 connectorStatus
.transactionStarted
= false;
307 delete connectorStatus
?.transactionStart
;
308 delete connectorStatus
?.transactionId
;
309 delete connectorStatus
?.localAuthorizeIdTag
;
310 delete connectorStatus
?.authorizeIdTag
;
311 delete connectorStatus
?.transactionIdTag
;
312 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0;
313 delete connectorStatus
?.transactionBeginMeterValue
;
316 export const createBootNotificationRequest
= (
317 stationInfo
: ChargingStationInfo
,
318 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
,
319 ): BootNotificationRequest
=> {
320 const ocppVersion
= stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
;
321 switch (ocppVersion
) {
322 case OCPPVersion
.VERSION_16
:
324 chargePointModel
: stationInfo
.chargePointModel
,
325 chargePointVendor
: stationInfo
.chargePointVendor
,
326 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
327 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
329 ...(!isUndefined(stationInfo
.chargePointSerialNumber
) && {
330 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
332 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
333 firmwareVersion
: stationInfo
.firmwareVersion
,
335 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
336 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
337 ...(!isUndefined(stationInfo
.meterSerialNumber
) && {
338 meterSerialNumber
: stationInfo
.meterSerialNumber
,
340 ...(!isUndefined(stationInfo
.meterType
) && {
341 meterType
: stationInfo
.meterType
,
343 } as OCPP16BootNotificationRequest
;
344 case OCPPVersion
.VERSION_20
:
345 case OCPPVersion
.VERSION_201
:
349 model
: stationInfo
.chargePointModel
,
350 vendorName
: stationInfo
.chargePointVendor
,
351 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
352 firmwareVersion
: stationInfo
.firmwareVersion
,
354 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
355 serialNumber
: stationInfo
.chargeBoxSerialNumber
,
357 ...((!isUndefined(stationInfo
.iccid
) || !isUndefined(stationInfo
.imsi
)) && {
359 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
360 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
364 } as OCPP20BootNotificationRequest
;
368 export const warnTemplateKeysDeprecation
= (
369 stationTemplate
: ChargingStationTemplate
,
371 templateFile
: string,
373 const templateKeys
: { deprecatedKey
: string; key
?: string }[] = [
374 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
375 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
376 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
378 for (const templateKey
of templateKeys
) {
379 warnDeprecatedTemplateKey(
381 templateKey
.deprecatedKey
,
384 !isUndefined(templateKey
.key
) ? `Use '${templateKey.key}' instead` : undefined,
386 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
);
390 export const stationTemplateToStationInfo
= (
391 stationTemplate
: ChargingStationTemplate
,
392 ): ChargingStationInfo
=> {
393 stationTemplate
= cloneObject
<ChargingStationTemplate
>(stationTemplate
);
394 delete stationTemplate
.power
;
395 delete stationTemplate
.powerUnit
;
396 delete stationTemplate
.Connectors
;
397 delete stationTemplate
.Evses
;
398 delete stationTemplate
.Configuration
;
399 delete stationTemplate
.AutomaticTransactionGenerator
;
400 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
401 delete stationTemplate
.chargePointSerialNumberPrefix
;
402 delete stationTemplate
.meterSerialNumberPrefix
;
403 return stationTemplate
as ChargingStationInfo
;
406 export const createSerialNumber
= (
407 stationTemplate
: ChargingStationTemplate
,
408 stationInfo
: ChargingStationInfo
,
410 randomSerialNumberUpperCase
?: boolean;
411 randomSerialNumber
?: boolean;
413 randomSerialNumberUpperCase
: true,
414 randomSerialNumber
: true,
417 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
};
418 const serialNumberSuffix
= params
?.randomSerialNumber
419 ? getRandomSerialNumberSuffix({
420 upperCase
: params
.randomSerialNumberUpperCase
,
423 isNotEmptyString(stationTemplate
?.chargePointSerialNumberPrefix
) &&
424 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
425 isNotEmptyString(stationTemplate
?.chargeBoxSerialNumberPrefix
) &&
426 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
427 isNotEmptyString(stationTemplate
?.meterSerialNumberPrefix
) &&
428 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
431 export const propagateSerialNumber
= (
432 stationTemplate
: ChargingStationTemplate
,
433 stationInfoSrc
: ChargingStationInfo
,
434 stationInfoDst
: ChargingStationInfo
,
436 if (!stationInfoSrc
|| !stationTemplate
) {
438 'Missing charging station template or existing configuration to propagate serial number',
441 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
442 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
443 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
444 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
445 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
446 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
447 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
448 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
449 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
452 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
454 switch (stationInfo
.amperageLimitationUnit
) {
455 case AmpereUnits
.DECI_AMPERE
:
458 case AmpereUnits
.CENTI_AMPERE
:
461 case AmpereUnits
.MILLI_AMPERE
:
468 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
469 chargingStation
: ChargingStation
,
471 ): number | undefined => {
472 let limit
: number | undefined, matchingChargingProfile
: ChargingProfile
| undefined;
473 // Get charging profiles for connector id and sort by stack level
474 const chargingProfiles
=
475 cloneObject
<ChargingProfile
[]>(
476 chargingStation
.getConnectorStatus(connectorId
)!.chargingProfiles
!,
477 )?.sort((a
, b
) => b
.stackLevel
- a
.stackLevel
) ?? [];
478 // Get charging profiles on connector 0 and sort by stack level
479 if (isNotEmptyArray(chargingStation
.getConnectorStatus(0)?.chargingProfiles
)) {
480 chargingProfiles
.push(
481 ...cloneObject
<ChargingProfile
[]>(
482 chargingStation
.getConnectorStatus(0)!.chargingProfiles
!,
483 ).sort((a
, b
) => b
.stackLevel
- a
.stackLevel
),
486 if (isNotEmptyArray(chargingProfiles
)) {
487 const result
= getLimitFromChargingProfiles(
491 chargingStation
.logPrefix(),
493 if (!isNullOrUndefined(result
)) {
494 limit
= result
?.limit
;
495 matchingChargingProfile
= result
?.matchingChargingProfile
;
496 switch (chargingStation
.getCurrentOutType()) {
499 matchingChargingProfile
?.chargingSchedule
?.chargingRateUnit
===
500 ChargingRateUnitType
.WATT
502 : ACElectricUtils
.powerTotal(
503 chargingStation
.getNumberOfPhases(),
504 chargingStation
.getVoltageOut(),
510 matchingChargingProfile
?.chargingSchedule
?.chargingRateUnit
===
511 ChargingRateUnitType
.WATT
513 : DCElectricUtils
.power(chargingStation
.getVoltageOut(), limit
!);
515 const connectorMaximumPower
=
516 chargingStation
.getMaximumPower() / chargingStation
.powerDivider
;
517 if (limit
! > connectorMaximumPower
) {
519 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${matchingChargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
522 limit
= connectorMaximumPower
;
529 export const getDefaultVoltageOut
= (
530 currentType
: CurrentType
,
532 templateFile
: string,
534 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
535 let defaultVoltageOut
: number;
536 switch (currentType
) {
538 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
541 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
544 logger
.error(`${logPrefix} ${errorMsg}`);
545 throw new BaseError(errorMsg
);
547 return defaultVoltageOut
;
550 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
552 stationInfo
.idTagsFile
&&
553 join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
557 export const waitChargingStationEvents
= async (
558 emitter
: EventEmitter
,
559 event
: ChargingStationWorkerMessageEvents
,
560 eventsToWait
: number,
561 ): Promise
<number> => {
562 return new Promise
<number>((resolve
) => {
564 if (eventsToWait
=== 0) {
567 emitter
.on(event
, () => {
569 if (events
=== eventsToWait
) {
576 const getConfiguredNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
577 let configuredMaxConnectors
= 0;
578 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
) === true) {
579 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
580 configuredMaxConnectors
=
581 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)];
582 } else if (isUndefined(stationTemplate
.numberOfConnectors
) === false) {
583 configuredMaxConnectors
= stationTemplate
.numberOfConnectors
as number;
584 } else if (stationTemplate
.Connectors
&& !stationTemplate
.Evses
) {
585 configuredMaxConnectors
= stationTemplate
.Connectors
[0]
586 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
587 : getMaxNumberOfConnectors(stationTemplate
.Connectors
);
588 } else if (stationTemplate
.Evses
&& !stationTemplate
.Connectors
) {
589 for (const evse
in stationTemplate
.Evses
) {
593 configuredMaxConnectors
+= getMaxNumberOfConnectors(stationTemplate
.Evses
[evse
].Connectors
);
596 return configuredMaxConnectors
;
599 const checkConfiguredMaxConnectors
= (
600 configuredMaxConnectors
: number,
602 templateFile
: string,
604 if (configuredMaxConnectors
<= 0) {
606 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
611 const checkTemplateMaxConnectors
= (
612 templateMaxConnectors
: number,
614 templateFile
: string,
616 if (templateMaxConnectors
=== 0) {
618 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
620 } else if (templateMaxConnectors
< 0) {
622 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
627 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
628 connectorStatus
.availability
= AvailabilityType
.Operative
;
629 connectorStatus
.idTagLocalAuthorized
= false;
630 connectorStatus
.idTagAuthorized
= false;
631 connectorStatus
.transactionRemoteStarted
= false;
632 connectorStatus
.transactionStarted
= false;
633 connectorStatus
.energyActiveImportRegisterValue
= 0;
634 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0;
635 if (isUndefined(connectorStatus
.chargingProfiles
)) {
636 connectorStatus
.chargingProfiles
= [];
640 const warnDeprecatedTemplateKey
= (
641 template
: ChargingStationTemplate
,
644 templateFile
: string,
647 if (!isUndefined(template
[key
as keyof ChargingStationTemplate
])) {
648 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
649 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
651 logger
.warn(`${logPrefix} ${logMsg}`);
652 console
.warn(chalk
.yellow(`${logMsg}`));
656 const convertDeprecatedTemplateKey
= (
657 template
: ChargingStationTemplate
,
658 deprecatedKey
: string,
661 if (!isUndefined(template
[deprecatedKey
as keyof ChargingStationTemplate
])) {
662 if (!isUndefined(key
)) {
663 (template
as unknown
as Record
<string, unknown
>)[key
!] =
664 template
[deprecatedKey
as keyof ChargingStationTemplate
];
666 delete template
[deprecatedKey
as keyof ChargingStationTemplate
];
670 interface ChargingProfilesLimit
{
672 matchingChargingProfile
: ChargingProfile
;
676 * Charging profiles shall already be sorted by connector id and stack level (highest stack level has priority)
678 * @param chargingStation -
679 * @param connectorId -
680 * @param chargingProfiles -
682 * @returns ChargingProfilesLimit
684 const getLimitFromChargingProfiles
= (
685 chargingStation
: ChargingStation
,
687 chargingProfiles
: ChargingProfile
[],
689 ): ChargingProfilesLimit
| undefined => {
690 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
691 const currentDate
= new Date();
692 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
);
693 for (const chargingProfile
of chargingProfiles
) {
694 const chargingSchedule
= chargingProfile
.chargingSchedule
;
695 if (connectorStatus
?.transactionStarted
&& isNullOrUndefined(chargingSchedule
?.startSchedule
)) {
697 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`,
699 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
700 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
;
702 if (!isDate(chargingSchedule
?.startSchedule
)) {
704 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date object. Trying to convert it to a Date object`,
706 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
?.startSchedule
)!;
708 switch (chargingProfile
.chargingProfileKind
) {
709 case ChargingProfileKindType
.RECURRING
:
710 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
713 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
);
715 case ChargingProfileKindType
.RELATIVE
:
716 connectorStatus
?.transactionStarted
&&
717 (chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
);
720 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
723 // Check if the charging profile is active
725 isValidTime(chargingSchedule
?.startSchedule
) &&
726 isWithinInterval(currentDate
, {
727 start
: chargingSchedule
.startSchedule
!,
728 end
: addSeconds(chargingSchedule
.startSchedule
!, chargingSchedule
.duration
!),
731 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
732 const chargingSchedulePeriodCompareFn
= (
733 a
: ChargingSchedulePeriod
,
734 b
: ChargingSchedulePeriod
,
735 ) => a
.startPeriod
- b
.startPeriod
;
737 isArraySorted
<ChargingSchedulePeriod
>(
738 chargingSchedule
.chargingSchedulePeriod
,
739 chargingSchedulePeriodCompareFn
,
743 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
745 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
);
747 // Check if the first schedule period start period is equal to 0
748 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
750 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`,
754 // Handle only one schedule period
755 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
756 const result
: ChargingProfilesLimit
= {
757 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
758 matchingChargingProfile
: chargingProfile
,
760 logger
.debug(debugLogMsg
, result
);
763 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined;
764 // Search for the right schedule period
767 chargingSchedulePeriod
,
768 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
769 // Find the right schedule period
772 addSeconds(chargingSchedule
.startSchedule
!, chargingSchedulePeriod
.startPeriod
),
776 // Found the schedule period: previous is the correct one
777 const result
: ChargingProfilesLimit
= {
778 limit
: previousChargingSchedulePeriod
!.limit
,
779 matchingChargingProfile
: chargingProfile
,
781 logger
.debug(debugLogMsg
, result
);
784 // Keep a reference to previous one
785 previousChargingSchedulePeriod
= chargingSchedulePeriod
;
786 // Handle the last schedule period within the charging profile duration
788 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
789 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
790 chargingSchedule
.duration
! >
793 chargingSchedule
.startSchedule
!,
794 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
,
796 chargingSchedule
.startSchedule
!,
799 const result
: ChargingProfilesLimit
= {
800 limit
: previousChargingSchedulePeriod
.limit
,
801 matchingChargingProfile
: chargingProfile
,
803 logger
.debug(debugLogMsg
, result
);
812 const canProceedChargingProfile
= (
813 chargingProfile
: ChargingProfile
,
818 (isValidTime(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
!)) ||
819 (isValidTime(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
!))
822 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
823 chargingProfile.chargingProfileId
824 } is not valid for the current date ${currentDate.toISOString()}`,
828 const chargingSchedule
= chargingProfile
.chargingSchedule
;
829 if (isNullOrUndefined(chargingSchedule
?.startSchedule
)) {
831 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has (still) no startSchedule defined`,
835 if (isNullOrUndefined(chargingSchedule
?.duration
)) {
837 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined, not yet supported`,
844 const canProceedRecurringChargingProfile
= (
845 chargingProfile
: ChargingProfile
,
849 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
850 isNullOrUndefined(chargingProfile
.recurrencyKind
)
853 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
861 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
863 * @param chargingProfile -
864 * @param currentDate -
867 const prepareRecurringChargingProfile
= (
868 chargingProfile
: ChargingProfile
,
872 const chargingSchedule
= chargingProfile
.chargingSchedule
;
873 let recurringIntervalTranslated
= false;
874 let recurringInterval
: Interval
;
875 switch (chargingProfile
.recurrencyKind
) {
876 case RecurrencyKindType
.DAILY
:
877 recurringInterval
= {
878 start
: chargingSchedule
.startSchedule
!,
879 end
: addDays(chargingSchedule
.startSchedule
!, 1),
881 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
);
883 !isWithinInterval(currentDate
, recurringInterval
) &&
884 isBefore(recurringInterval
.end
, currentDate
)
886 chargingSchedule
.startSchedule
= addDays(
887 recurringInterval
.start
,
888 differenceInDays(currentDate
, recurringInterval
.start
),
890 recurringInterval
= {
891 start
: chargingSchedule
.startSchedule
,
892 end
: addDays(chargingSchedule
.startSchedule
, 1),
894 recurringIntervalTranslated
= true;
897 case RecurrencyKindType
.WEEKLY
:
898 recurringInterval
= {
899 start
: chargingSchedule
.startSchedule
!,
900 end
: addWeeks(chargingSchedule
.startSchedule
!, 1),
902 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
);
904 !isWithinInterval(currentDate
, recurringInterval
) &&
905 isBefore(recurringInterval
.end
, currentDate
)
907 chargingSchedule
.startSchedule
= addWeeks(
908 recurringInterval
.start
,
909 differenceInWeeks(currentDate
, recurringInterval
.start
),
911 recurringInterval
= {
912 start
: chargingSchedule
.startSchedule
,
913 end
: addWeeks(chargingSchedule
.startSchedule
, 1),
915 recurringIntervalTranslated
= true;
920 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} recurrency kind ${chargingProfile.recurrencyKind} is not supported`,
923 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
925 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
926 chargingProfile.recurrencyKind
927 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
928 recurringInterval!.start,
929 ).toISOString()}, ${toDate(
930 recurringInterval!.end,
931 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
934 return recurringIntervalTranslated
;
937 const checkRecurringChargingProfileDuration
= (
938 chargingProfile
: ChargingProfile
,
942 if (isNullOrUndefined(chargingProfile
.chargingSchedule
.duration
)) {
944 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
945 chargingProfile.chargingProfileKind
946 } charging profile id ${
947 chargingProfile.chargingProfileId
948 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
953 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
);
955 chargingProfile
.chargingSchedule
.duration
! > differenceInSeconds(interval
.end
, interval
.start
)
958 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
959 chargingProfile.chargingProfileKind
960 } charging profile id ${chargingProfile.chargingProfileId} duration ${
961 chargingProfile.chargingSchedule.duration
962 } is greater than the recurrency time interval duration ${differenceInSeconds(
967 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
);
971 const getRandomSerialNumberSuffix
= (params
?: {
972 randomBytesLength
?: number;
975 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex');
976 if (params
?.upperCase
) {
977 return randomSerialNumberSuffix
.toUpperCase();
979 return randomSerialNumberSuffix
;