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 { getConfigurationKey
} from
'./ConfigurationKeyUtils';
23 import { BaseError
} from
'../exception';
27 type BootNotificationRequest
,
30 ChargingProfileKindType
,
32 type ChargingSchedulePeriod
,
33 type ChargingStationInfo
,
34 type ChargingStationTemplate
,
35 ChargingStationWorkerMessageEvents
,
36 ConnectorPhaseRotation
,
41 type OCPP16BootNotificationRequest
,
42 type OCPP20BootNotificationRequest
,
45 StandardParametersKey
,
46 SupportedFeatureProfiles
,
68 const moduleName
= 'Helpers';
70 export const getChargingStationId
= (
72 stationTemplate
: ChargingStationTemplate
,
74 // In case of multiple instances: add instance index to charging station id
75 const instanceIndex
= process
.env
.CF_INSTANCE_INDEX
?? 0;
76 const idSuffix
= stationTemplate
?.nameSuffix
?? '';
77 const idStr
= `000000000${index.toString()}`;
78 return stationTemplate
?.fixedName
79 ? stationTemplate
.baseName
80 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
85 export const countReservableConnectors
= (connectors
: Map
<number, ConnectorStatus
>) => {
86 let reservableConnectors
= 0;
87 for (const [connectorId
, connectorStatus
] of connectors
) {
88 if (connectorId
=== 0) {
91 if (connectorStatus
.status === ConnectorStatusEnum
.Available
) {
92 ++reservableConnectors
;
95 return reservableConnectors
;
98 export const getHashId
= (index
: number, stationTemplate
: ChargingStationTemplate
): string => {
99 const chargingStationInfo
= {
100 chargePointModel
: stationTemplate
.chargePointModel
,
101 chargePointVendor
: stationTemplate
.chargePointVendor
,
102 ...(!isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
103 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
105 ...(!isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
106 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
108 ...(!isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
109 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
111 ...(!isUndefined(stationTemplate
.meterType
) && {
112 meterType
: stationTemplate
.meterType
,
115 return createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
116 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
120 export const checkChargingStation
= (
121 chargingStation
: ChargingStation
,
124 if (chargingStation
.started
=== false && chargingStation
.starting
=== false) {
125 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`);
131 export const getPhaseRotationValue
= (
133 numberOfPhases
: number,
134 ): string | undefined => {
136 if (connectorId
=== 0 && numberOfPhases
=== 0) {
137 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
138 } else if (connectorId
> 0 && numberOfPhases
=== 0) {
139 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
141 } else if (connectorId
> 0 && numberOfPhases
=== 1) {
142 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
143 } else if (connectorId
> 0 && numberOfPhases
=== 3) {
144 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
148 export const getMaxNumberOfEvses
= (evses
: Record
<string, EvseTemplate
>): number => {
152 return Object.keys(evses
).length
;
155 const getMaxNumberOfConnectors
= (connectors
: Record
<string, ConnectorStatus
>): number => {
159 return Object.keys(connectors
).length
;
162 export const getBootConnectorStatus
= (
163 chargingStation
: ChargingStation
,
165 connectorStatus
: ConnectorStatus
,
166 ): ConnectorStatusEnum
=> {
167 let connectorBootStatus
: ConnectorStatusEnum
;
169 !connectorStatus
?.status &&
170 (chargingStation
.isChargingStationAvailable() === false ||
171 chargingStation
.isConnectorAvailable(connectorId
) === false)
173 connectorBootStatus
= ConnectorStatusEnum
.Unavailable
;
174 } else if (!connectorStatus
?.status && connectorStatus
?.bootStatus
) {
175 // Set boot status in template at startup
176 connectorBootStatus
= connectorStatus
?.bootStatus
;
177 } else if (connectorStatus
?.status) {
178 // Set previous status at startup
179 connectorBootStatus
= connectorStatus
?.status;
181 // Set default status
182 connectorBootStatus
= ConnectorStatusEnum
.Available
;
184 return connectorBootStatus
;
187 export const checkTemplate
= (
188 stationTemplate
: ChargingStationTemplate
,
190 templateFile
: string,
192 if (isNullOrUndefined(stationTemplate
)) {
193 const errorMsg
= `Failed to read charging station template file ${templateFile}`;
194 logger
.error(`${logPrefix} ${errorMsg}`);
195 throw new BaseError(errorMsg
);
197 if (isEmptyObject(stationTemplate
)) {
198 const errorMsg
= `Empty charging station information from template file ${templateFile}`;
199 logger
.error(`${logPrefix} ${errorMsg}`);
200 throw new BaseError(errorMsg
);
202 if (isEmptyObject(stationTemplate
.AutomaticTransactionGenerator
!)) {
203 stationTemplate
.AutomaticTransactionGenerator
= Constants
.DEFAULT_ATG_CONFIGURATION
;
205 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
206 Constants
.DEFAULT_ATG_CONFIGURATION
,
209 if (isNullOrUndefined(stationTemplate
.idTagsFile
) || isEmptyString(stationTemplate
.idTagsFile
)) {
211 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`,
216 export const checkConnectorsConfiguration
= (
217 stationTemplate
: ChargingStationTemplate
,
219 templateFile
: string,
221 configuredMaxConnectors
: number;
222 templateMaxConnectors
: number;
223 templateMaxAvailableConnectors
: number;
225 const configuredMaxConnectors
= getConfiguredNumberOfConnectors(stationTemplate
);
226 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
);
227 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
!);
228 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
);
229 const templateMaxAvailableConnectors
= stationTemplate
.Connectors
![0]
230 ? templateMaxConnectors
- 1
231 : templateMaxConnectors
;
233 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
234 !stationTemplate
?.randomConnectors
237 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`,
239 stationTemplate
.randomConnectors
= true;
241 return { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
};
244 export const checkStationInfoConnectorStatus
= (
246 connectorStatus
: ConnectorStatus
,
248 templateFile
: string,
250 if (!isNullOrUndefined(connectorStatus
?.status)) {
252 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`,
254 delete connectorStatus
.status;
258 export const buildConnectorsMap
= (
259 connectors
: Record
<string, ConnectorStatus
>,
261 templateFile
: string,
262 ): Map
<number, ConnectorStatus
> => {
263 const connectorsMap
= new Map
<number, ConnectorStatus
>();
264 if (getMaxNumberOfConnectors(connectors
) > 0) {
265 for (const connector
in connectors
) {
266 const connectorStatus
= connectors
[connector
];
267 const connectorId
= convertToInt(connector
);
268 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
);
269 connectorsMap
.set(connectorId
, cloneObject
<ConnectorStatus
>(connectorStatus
));
273 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`,
276 return connectorsMap
;
279 export const initializeConnectorsMapStatus
= (
280 connectors
: Map
<number, ConnectorStatus
>,
283 for (const connectorId
of connectors
.keys()) {
284 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
286 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
291 if (connectorId
=== 0) {
292 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
;
293 if (isUndefined(connectors
.get(connectorId
)?.chargingProfiles
)) {
294 connectors
.get(connectorId
)!.chargingProfiles
= [];
298 isNullOrUndefined(connectors
.get(connectorId
)?.transactionStarted
)
300 initializeConnectorStatus(connectors
.get(connectorId
)!);
305 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
306 connectorStatus
.idTagLocalAuthorized
= false;
307 connectorStatus
.idTagAuthorized
= false;
308 connectorStatus
.transactionRemoteStarted
= false;
309 connectorStatus
.transactionStarted
= false;
310 delete connectorStatus
?.transactionStart
;
311 delete connectorStatus
?.transactionId
;
312 delete connectorStatus
?.localAuthorizeIdTag
;
313 delete connectorStatus
?.authorizeIdTag
;
314 delete connectorStatus
?.transactionIdTag
;
315 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0;
316 delete connectorStatus
?.transactionBeginMeterValue
;
319 export const createBootNotificationRequest
= (
320 stationInfo
: ChargingStationInfo
,
321 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
,
322 ): BootNotificationRequest
=> {
323 const ocppVersion
= stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
;
324 switch (ocppVersion
) {
325 case OCPPVersion
.VERSION_16
:
327 chargePointModel
: stationInfo
.chargePointModel
,
328 chargePointVendor
: stationInfo
.chargePointVendor
,
329 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
330 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
332 ...(!isUndefined(stationInfo
.chargePointSerialNumber
) && {
333 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
335 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
336 firmwareVersion
: stationInfo
.firmwareVersion
,
338 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
339 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
340 ...(!isUndefined(stationInfo
.meterSerialNumber
) && {
341 meterSerialNumber
: stationInfo
.meterSerialNumber
,
343 ...(!isUndefined(stationInfo
.meterType
) && {
344 meterType
: stationInfo
.meterType
,
346 } as OCPP16BootNotificationRequest
;
347 case OCPPVersion
.VERSION_20
:
348 case OCPPVersion
.VERSION_201
:
352 model
: stationInfo
.chargePointModel
,
353 vendorName
: stationInfo
.chargePointVendor
,
354 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
355 firmwareVersion
: stationInfo
.firmwareVersion
,
357 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
358 serialNumber
: stationInfo
.chargeBoxSerialNumber
,
360 ...((!isUndefined(stationInfo
.iccid
) || !isUndefined(stationInfo
.imsi
)) && {
362 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
363 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
367 } as OCPP20BootNotificationRequest
;
371 export const warnTemplateKeysDeprecation
= (
372 stationTemplate
: ChargingStationTemplate
,
374 templateFile
: string,
376 const templateKeys
: { deprecatedKey
: string; key
?: string }[] = [
377 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
378 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
379 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
380 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' },
382 for (const templateKey
of templateKeys
) {
383 warnDeprecatedTemplateKey(
385 templateKey
.deprecatedKey
,
388 !isUndefined(templateKey
.key
) ? `Use '${templateKey.key}' instead` : undefined,
390 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
);
394 export const stationTemplateToStationInfo
= (
395 stationTemplate
: ChargingStationTemplate
,
396 ): ChargingStationInfo
=> {
397 stationTemplate
= cloneObject
<ChargingStationTemplate
>(stationTemplate
);
398 delete stationTemplate
.power
;
399 delete stationTemplate
.powerUnit
;
400 delete stationTemplate
.Connectors
;
401 delete stationTemplate
.Evses
;
402 delete stationTemplate
.Configuration
;
403 delete stationTemplate
.AutomaticTransactionGenerator
;
404 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
405 delete stationTemplate
.chargePointSerialNumberPrefix
;
406 delete stationTemplate
.meterSerialNumberPrefix
;
407 return stationTemplate
as ChargingStationInfo
;
410 export const createSerialNumber
= (
411 stationTemplate
: ChargingStationTemplate
,
412 stationInfo
: ChargingStationInfo
,
414 randomSerialNumberUpperCase
?: boolean;
415 randomSerialNumber
?: boolean;
417 randomSerialNumberUpperCase
: true,
418 randomSerialNumber
: true,
421 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
};
422 const serialNumberSuffix
= params
?.randomSerialNumber
423 ? getRandomSerialNumberSuffix({
424 upperCase
: params
.randomSerialNumberUpperCase
,
427 isNotEmptyString(stationTemplate
?.chargePointSerialNumberPrefix
) &&
428 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
429 isNotEmptyString(stationTemplate
?.chargeBoxSerialNumberPrefix
) &&
430 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
431 isNotEmptyString(stationTemplate
?.meterSerialNumberPrefix
) &&
432 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
435 export const propagateSerialNumber
= (
436 stationTemplate
: ChargingStationTemplate
,
437 stationInfoSrc
: ChargingStationInfo
,
438 stationInfoDst
: ChargingStationInfo
,
440 if (!stationInfoSrc
|| !stationTemplate
) {
442 'Missing charging station template or existing configuration to propagate serial number',
445 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
446 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
447 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
448 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
449 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
450 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
451 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
452 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
453 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
456 export const hasFeatureProfile
= (
457 chargingStation
: ChargingStation
,
458 featureProfile
: SupportedFeatureProfiles
,
459 ): boolean | undefined => {
460 return getConfigurationKey(
462 StandardParametersKey
.SupportedFeatureProfiles
,
463 )?.value
?.includes(featureProfile
);
466 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
468 switch (stationInfo
.amperageLimitationUnit
) {
469 case AmpereUnits
.DECI_AMPERE
:
472 case AmpereUnits
.CENTI_AMPERE
:
475 case AmpereUnits
.MILLI_AMPERE
:
482 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
483 chargingStation
: ChargingStation
,
485 ): number | undefined => {
486 let limit
: number | undefined, matchingChargingProfile
: ChargingProfile
| undefined;
487 // Get charging profiles for connector id and sort by stack level
488 const chargingProfiles
=
489 cloneObject
<ChargingProfile
[]>(
490 chargingStation
.getConnectorStatus(connectorId
)!.chargingProfiles
!,
491 )?.sort((a
, b
) => b
.stackLevel
- a
.stackLevel
) ?? [];
492 // Get charging profiles on connector 0 and sort by stack level
493 if (isNotEmptyArray(chargingStation
.getConnectorStatus(0)?.chargingProfiles
)) {
494 chargingProfiles
.push(
495 ...cloneObject
<ChargingProfile
[]>(
496 chargingStation
.getConnectorStatus(0)!.chargingProfiles
!,
497 ).sort((a
, b
) => b
.stackLevel
- a
.stackLevel
),
500 if (isNotEmptyArray(chargingProfiles
)) {
501 const result
= getLimitFromChargingProfiles(
505 chargingStation
.logPrefix(),
507 if (!isNullOrUndefined(result
)) {
508 limit
= result
?.limit
;
509 matchingChargingProfile
= result
?.matchingChargingProfile
;
510 switch (chargingStation
.getCurrentOutType()) {
513 matchingChargingProfile
?.chargingSchedule
?.chargingRateUnit
===
514 ChargingRateUnitType
.WATT
516 : ACElectricUtils
.powerTotal(
517 chargingStation
.getNumberOfPhases(),
518 chargingStation
.getVoltageOut(),
524 matchingChargingProfile
?.chargingSchedule
?.chargingRateUnit
===
525 ChargingRateUnitType
.WATT
527 : DCElectricUtils
.power(chargingStation
.getVoltageOut(), limit
!);
529 const connectorMaximumPower
=
530 chargingStation
.getMaximumPower() / chargingStation
.powerDivider
;
531 if (limit
! > connectorMaximumPower
) {
533 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${matchingChargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
536 limit
= connectorMaximumPower
;
543 export const getDefaultVoltageOut
= (
544 currentType
: CurrentType
,
546 templateFile
: string,
548 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
549 let defaultVoltageOut
: number;
550 switch (currentType
) {
552 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
555 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
558 logger
.error(`${logPrefix} ${errorMsg}`);
559 throw new BaseError(errorMsg
);
561 return defaultVoltageOut
;
564 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
566 stationInfo
.idTagsFile
&&
567 join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
571 export const waitChargingStationEvents
= async (
572 emitter
: EventEmitter
,
573 event
: ChargingStationWorkerMessageEvents
,
574 eventsToWait
: number,
575 ): Promise
<number> => {
576 return new Promise
<number>((resolve
) => {
578 if (eventsToWait
=== 0) {
581 emitter
.on(event
, () => {
583 if (events
=== eventsToWait
) {
590 const getConfiguredNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
591 let configuredMaxConnectors
= 0;
592 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
) === true) {
593 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
594 configuredMaxConnectors
=
595 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)];
596 } else if (isUndefined(stationTemplate
.numberOfConnectors
) === false) {
597 configuredMaxConnectors
= stationTemplate
.numberOfConnectors
as number;
598 } else if (stationTemplate
.Connectors
&& !stationTemplate
.Evses
) {
599 configuredMaxConnectors
= stationTemplate
.Connectors
[0]
600 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
601 : getMaxNumberOfConnectors(stationTemplate
.Connectors
);
602 } else if (stationTemplate
.Evses
&& !stationTemplate
.Connectors
) {
603 for (const evse
in stationTemplate
.Evses
) {
607 configuredMaxConnectors
+= getMaxNumberOfConnectors(stationTemplate
.Evses
[evse
].Connectors
);
610 return configuredMaxConnectors
;
613 const checkConfiguredMaxConnectors
= (
614 configuredMaxConnectors
: number,
616 templateFile
: string,
618 if (configuredMaxConnectors
<= 0) {
620 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
625 const checkTemplateMaxConnectors
= (
626 templateMaxConnectors
: number,
628 templateFile
: string,
630 if (templateMaxConnectors
=== 0) {
632 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
634 } else if (templateMaxConnectors
< 0) {
636 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
641 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
642 connectorStatus
.availability
= AvailabilityType
.Operative
;
643 connectorStatus
.idTagLocalAuthorized
= false;
644 connectorStatus
.idTagAuthorized
= false;
645 connectorStatus
.transactionRemoteStarted
= false;
646 connectorStatus
.transactionStarted
= false;
647 connectorStatus
.energyActiveImportRegisterValue
= 0;
648 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0;
649 if (isUndefined(connectorStatus
.chargingProfiles
)) {
650 connectorStatus
.chargingProfiles
= [];
654 const warnDeprecatedTemplateKey
= (
655 template
: ChargingStationTemplate
,
658 templateFile
: string,
661 if (!isUndefined(template
[key
as keyof ChargingStationTemplate
])) {
662 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
663 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
665 logger
.warn(`${logPrefix} ${logMsg}`);
666 console
.warn(chalk
.yellow(`${logMsg}`));
670 const convertDeprecatedTemplateKey
= (
671 template
: ChargingStationTemplate
,
672 deprecatedKey
: string,
675 if (!isUndefined(template
[deprecatedKey
as keyof ChargingStationTemplate
])) {
676 if (!isUndefined(key
)) {
677 (template
as unknown
as Record
<string, unknown
>)[key
!] =
678 template
[deprecatedKey
as keyof ChargingStationTemplate
];
680 delete template
[deprecatedKey
as keyof ChargingStationTemplate
];
684 interface ChargingProfilesLimit
{
686 matchingChargingProfile
: ChargingProfile
;
690 * Charging profiles shall already be sorted by connector id and stack level (highest stack level has priority)
692 * @param chargingStation -
693 * @param connectorId -
694 * @param chargingProfiles -
696 * @returns ChargingProfilesLimit
698 const getLimitFromChargingProfiles
= (
699 chargingStation
: ChargingStation
,
701 chargingProfiles
: ChargingProfile
[],
703 ): ChargingProfilesLimit
| undefined => {
704 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
705 const currentDate
= new Date();
706 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
);
707 for (const chargingProfile
of chargingProfiles
) {
708 const chargingSchedule
= chargingProfile
.chargingSchedule
;
709 if (connectorStatus
?.transactionStarted
&& isNullOrUndefined(chargingSchedule
?.startSchedule
)) {
711 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`,
713 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
714 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
;
716 if (!isDate(chargingSchedule
?.startSchedule
)) {
718 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date object. Trying to convert it to a Date object`,
720 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
?.startSchedule
)!;
722 switch (chargingProfile
.chargingProfileKind
) {
723 case ChargingProfileKindType
.RECURRING
:
724 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
727 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
);
729 case ChargingProfileKindType
.RELATIVE
:
730 connectorStatus
?.transactionStarted
&&
731 (chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
);
734 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
737 // Check if the charging profile is active
739 isValidTime(chargingSchedule
?.startSchedule
) &&
740 isWithinInterval(currentDate
, {
741 start
: chargingSchedule
.startSchedule
!,
742 end
: addSeconds(chargingSchedule
.startSchedule
!, chargingSchedule
.duration
!),
745 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
746 const chargingSchedulePeriodCompareFn
= (
747 a
: ChargingSchedulePeriod
,
748 b
: ChargingSchedulePeriod
,
749 ) => a
.startPeriod
- b
.startPeriod
;
751 isArraySorted
<ChargingSchedulePeriod
>(
752 chargingSchedule
.chargingSchedulePeriod
,
753 chargingSchedulePeriodCompareFn
,
757 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
759 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
);
761 // Check if the first schedule period start period is equal to 0
762 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
764 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`,
768 // Handle only one schedule period
769 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
770 const result
: ChargingProfilesLimit
= {
771 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
772 matchingChargingProfile
: chargingProfile
,
774 logger
.debug(debugLogMsg
, result
);
777 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined;
778 // Search for the right schedule period
781 chargingSchedulePeriod
,
782 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
783 // Find the right schedule period
786 addSeconds(chargingSchedule
.startSchedule
!, chargingSchedulePeriod
.startPeriod
),
790 // Found the schedule period: previous is the correct one
791 const result
: ChargingProfilesLimit
= {
792 limit
: previousChargingSchedulePeriod
!.limit
,
793 matchingChargingProfile
: chargingProfile
,
795 logger
.debug(debugLogMsg
, result
);
798 // Keep a reference to previous one
799 previousChargingSchedulePeriod
= chargingSchedulePeriod
;
800 // Handle the last schedule period within the charging profile duration
802 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
803 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
804 chargingSchedule
.duration
! >
807 chargingSchedule
.startSchedule
!,
808 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
,
810 chargingSchedule
.startSchedule
!,
813 const result
: ChargingProfilesLimit
= {
814 limit
: previousChargingSchedulePeriod
.limit
,
815 matchingChargingProfile
: chargingProfile
,
817 logger
.debug(debugLogMsg
, result
);
826 const canProceedChargingProfile
= (
827 chargingProfile
: ChargingProfile
,
832 (isValidTime(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
!)) ||
833 (isValidTime(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
!))
836 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
837 chargingProfile.chargingProfileId
838 } is not valid for the current date ${currentDate.toISOString()}`,
842 const chargingSchedule
= chargingProfile
.chargingSchedule
;
843 if (isNullOrUndefined(chargingSchedule
?.startSchedule
)) {
845 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has (still) no startSchedule defined`,
849 if (isNullOrUndefined(chargingSchedule
?.duration
)) {
851 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined, not yet supported`,
858 const canProceedRecurringChargingProfile
= (
859 chargingProfile
: ChargingProfile
,
863 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
864 isNullOrUndefined(chargingProfile
.recurrencyKind
)
867 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
875 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
877 * @param chargingProfile -
878 * @param currentDate -
881 const prepareRecurringChargingProfile
= (
882 chargingProfile
: ChargingProfile
,
886 const chargingSchedule
= chargingProfile
.chargingSchedule
;
887 let recurringIntervalTranslated
= false;
888 let recurringInterval
: Interval
;
889 switch (chargingProfile
.recurrencyKind
) {
890 case RecurrencyKindType
.DAILY
:
891 recurringInterval
= {
892 start
: chargingSchedule
.startSchedule
!,
893 end
: addDays(chargingSchedule
.startSchedule
!, 1),
895 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
);
897 !isWithinInterval(currentDate
, recurringInterval
) &&
898 isBefore(recurringInterval
.end
, currentDate
)
900 chargingSchedule
.startSchedule
= addDays(
901 recurringInterval
.start
,
902 differenceInDays(currentDate
, recurringInterval
.start
),
904 recurringInterval
= {
905 start
: chargingSchedule
.startSchedule
,
906 end
: addDays(chargingSchedule
.startSchedule
, 1),
908 recurringIntervalTranslated
= true;
911 case RecurrencyKindType
.WEEKLY
:
912 recurringInterval
= {
913 start
: chargingSchedule
.startSchedule
!,
914 end
: addWeeks(chargingSchedule
.startSchedule
!, 1),
916 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
);
918 !isWithinInterval(currentDate
, recurringInterval
) &&
919 isBefore(recurringInterval
.end
, currentDate
)
921 chargingSchedule
.startSchedule
= addWeeks(
922 recurringInterval
.start
,
923 differenceInWeeks(currentDate
, recurringInterval
.start
),
925 recurringInterval
= {
926 start
: chargingSchedule
.startSchedule
,
927 end
: addWeeks(chargingSchedule
.startSchedule
, 1),
929 recurringIntervalTranslated
= true;
934 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} recurrency kind ${chargingProfile.recurrencyKind} is not supported`,
937 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
939 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
940 chargingProfile.recurrencyKind
941 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
942 recurringInterval!.start,
943 ).toISOString()}, ${toDate(
944 recurringInterval!.end,
945 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
948 return recurringIntervalTranslated
;
951 const checkRecurringChargingProfileDuration
= (
952 chargingProfile
: ChargingProfile
,
956 if (isNullOrUndefined(chargingProfile
.chargingSchedule
.duration
)) {
958 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
959 chargingProfile.chargingProfileKind
960 } charging profile id ${
961 chargingProfile.chargingProfileId
962 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
967 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
);
969 chargingProfile
.chargingSchedule
.duration
! > differenceInSeconds(interval
.end
, interval
.start
)
972 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
973 chargingProfile.chargingProfileKind
974 } charging profile id ${chargingProfile.chargingProfileId} duration ${
975 chargingProfile.chargingSchedule.duration
976 } is greater than the recurrency time interval duration ${differenceInSeconds(
981 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
);
985 const getRandomSerialNumberSuffix
= (params
?: {
986 randomBytesLength
?: number;
989 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex');
990 if (params
?.upperCase
) {
991 return randomSerialNumberSuffix
.toUpperCase();
993 return randomSerialNumberSuffix
;