1 import crypto from
'node:crypto';
2 import path from
'node:path';
3 import { fileURLToPath
} from
'node:url';
5 import chalk from
'chalk';
6 import moment from
'moment';
8 import type { ChargingStation
} from
'./internal';
9 import { BaseError
} from
'../exception';
12 type BootNotificationRequest
,
15 ChargingProfileKindType
,
17 type ChargingSchedulePeriod
,
18 type ChargingStationInfo
,
19 type ChargingStationTemplate
,
22 type OCPP16BootNotificationRequest
,
23 type OCPP20BootNotificationRequest
,
36 import { WorkerProcessType
} from
'../worker';
38 const moduleName
= 'ChargingStationUtils';
40 export class ChargingStationUtils
{
41 private constructor() {
42 // This is intentional
45 public static getChargingStationId(
47 stationTemplate
: ChargingStationTemplate
49 // In case of multiple instances: add instance index to charging station id
50 const instanceIndex
= process
.env
.CF_INSTANCE_INDEX
?? 0;
51 const idSuffix
= stationTemplate
?.nameSuffix
?? '';
52 const idStr
= `000000000${index.toString()}`;
53 return stationTemplate
?.fixedName
54 ? stationTemplate
.baseName
55 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
60 public static getHashId(index
: number, stationTemplate
: ChargingStationTemplate
): string {
61 const chargingStationInfo
= {
62 chargePointModel
: stationTemplate
.chargePointModel
,
63 chargePointVendor
: stationTemplate
.chargePointVendor
,
64 ...(!Utils
.isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
65 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
67 ...(!Utils
.isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
68 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
70 ...(!Utils
.isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
71 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
73 ...(!Utils
.isUndefined(stationTemplate
.meterType
) && {
74 meterType
: stationTemplate
.meterType
,
78 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
80 `${JSON.stringify(chargingStationInfo)}${ChargingStationUtils.getChargingStationId(
88 public static checkChargingStation(chargingStation
: ChargingStation
, logPrefix
: string): boolean {
89 if (chargingStation
.started
=== false && chargingStation
.starting
=== false) {
90 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`);
96 public static getMaxNumberOfConnectors(connectors
: Record
<string, ConnectorStatus
>): number {
100 return Object.keys(connectors
).length
;
103 public static checkTemplateMaxConnectors(
104 templateMaxConnectors
: number,
105 templateFile
: string,
108 if (templateMaxConnectors
=== 0) {
110 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
112 } else if (templateMaxConnectors
< 0) {
114 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
119 public static getConfiguredNumberOfConnectors(stationTemplate
: ChargingStationTemplate
): number {
120 let configuredMaxConnectors
: number;
121 if (Utils
.isNotEmptyArray(stationTemplate
.numberOfConnectors
) === true) {
122 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
123 configuredMaxConnectors
=
124 numberOfConnectors
[Math.floor(Utils
.secureRandom() * numberOfConnectors
.length
)];
125 } else if (Utils
.isUndefined(stationTemplate
.numberOfConnectors
) === false) {
126 configuredMaxConnectors
= stationTemplate
.numberOfConnectors
as number;
127 } else if (stationTemplate
.Connectors
&& !stationTemplate
.Evses
) {
128 configuredMaxConnectors
= stationTemplate
?.Connectors
[0]
129 ? ChargingStationUtils
.getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
130 : ChargingStationUtils
.getMaxNumberOfConnectors(stationTemplate
.Connectors
);
131 } else if (stationTemplate
.Evses
&& !stationTemplate
.Connectors
) {
132 configuredMaxConnectors
= 0;
133 for (const evse
in stationTemplate
.Evses
) {
137 configuredMaxConnectors
+= ChargingStationUtils
.getMaxNumberOfConnectors(
138 stationTemplate
.Evses
[evse
].Connectors
142 return configuredMaxConnectors
;
145 public static checkConfiguredMaxConnectors(
146 configuredMaxConnectors
: number,
147 templateFile
: string,
150 if (configuredMaxConnectors
<= 0) {
152 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
157 public static createBootNotificationRequest(
158 stationInfo
: ChargingStationInfo
,
159 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
160 ): BootNotificationRequest
{
161 const ocppVersion
= stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
;
162 switch (ocppVersion
) {
163 case OCPPVersion
.VERSION_16
:
165 chargePointModel
: stationInfo
.chargePointModel
,
166 chargePointVendor
: stationInfo
.chargePointVendor
,
167 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
168 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
170 ...(!Utils
.isUndefined(stationInfo
.chargePointSerialNumber
) && {
171 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
173 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
174 firmwareVersion
: stationInfo
.firmwareVersion
,
176 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
177 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
178 ...(!Utils
.isUndefined(stationInfo
.meterSerialNumber
) && {
179 meterSerialNumber
: stationInfo
.meterSerialNumber
,
181 ...(!Utils
.isUndefined(stationInfo
.meterType
) && {
182 meterType
: stationInfo
.meterType
,
184 } as OCPP16BootNotificationRequest
;
185 case OCPPVersion
.VERSION_20
:
186 case OCPPVersion
.VERSION_201
:
190 model
: stationInfo
.chargePointModel
,
191 vendorName
: stationInfo
.chargePointVendor
,
192 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
193 firmwareVersion
: stationInfo
.firmwareVersion
,
195 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
196 serialNumber
: stationInfo
.chargeBoxSerialNumber
,
198 ...((!Utils
.isUndefined(stationInfo
.iccid
) || !Utils
.isUndefined(stationInfo
.imsi
)) && {
200 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
201 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
205 } as OCPP20BootNotificationRequest
;
209 public static workerPoolInUse(): boolean {
210 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.staticPool
].includes(
211 Configuration
.getWorker().processType
215 public static workerDynamicPoolInUse(): boolean {
216 return Configuration
.getWorker().processType
=== WorkerProcessType
.dynamicPool
;
219 public static warnTemplateKeysDeprecation(
220 templateFile
: string,
221 stationTemplate
: ChargingStationTemplate
,
224 const templateKeys
: { key
: string; deprecatedKey
: string }[] = [
225 { key
: 'supervisionUrls', deprecatedKey
: 'supervisionUrl' },
226 { key
: 'idTagsFile', deprecatedKey
: 'authorizationFile' },
228 for (const templateKey
of templateKeys
) {
229 ChargingStationUtils
.warnDeprecatedTemplateKey(
231 templateKey
.deprecatedKey
,
234 `Use '${templateKey.key}' instead`
236 ChargingStationUtils
.convertDeprecatedTemplateKey(
238 templateKey
.deprecatedKey
,
244 public static stationTemplateToStationInfo(
245 stationTemplate
: ChargingStationTemplate
246 ): ChargingStationInfo
{
247 stationTemplate
= Utils
.cloneObject(stationTemplate
);
248 delete stationTemplate
.power
;
249 delete stationTemplate
.powerUnit
;
250 delete stationTemplate
.Configuration
;
251 delete stationTemplate
.AutomaticTransactionGenerator
;
252 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
253 delete stationTemplate
.chargePointSerialNumberPrefix
;
254 delete stationTemplate
.meterSerialNumberPrefix
;
255 return stationTemplate
as unknown
as ChargingStationInfo
;
258 public static createStationInfoHash(stationInfo
: ChargingStationInfo
): void {
259 delete stationInfo
.infoHash
;
260 stationInfo
.infoHash
= crypto
261 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
262 .update(JSON
.stringify(stationInfo
))
266 public static createSerialNumber(
267 stationTemplate
: ChargingStationTemplate
,
268 stationInfo
: ChargingStationInfo
,
270 randomSerialNumberUpperCase
?: boolean;
271 randomSerialNumber
?: boolean;
273 randomSerialNumberUpperCase
: true,
274 randomSerialNumber
: true,
277 params
= params
?? {};
278 params
.randomSerialNumberUpperCase
= params
?.randomSerialNumberUpperCase
?? true;
279 params
.randomSerialNumber
= params
?.randomSerialNumber
?? true;
280 const serialNumberSuffix
= params
?.randomSerialNumber
281 ? ChargingStationUtils
.getRandomSerialNumberSuffix({
282 upperCase
: params
.randomSerialNumberUpperCase
,
285 stationInfo
.chargePointSerialNumber
= Utils
.isNotEmptyString(
286 stationTemplate
?.chargePointSerialNumberPrefix
288 ? `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`
290 stationInfo
.chargeBoxSerialNumber
= Utils
.isNotEmptyString(
291 stationTemplate
?.chargeBoxSerialNumberPrefix
293 ? `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`
295 stationInfo
.meterSerialNumber
= Utils
.isNotEmptyString(stationTemplate
?.meterSerialNumberPrefix
)
296 ? `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`
300 public static propagateSerialNumber(
301 stationTemplate
: ChargingStationTemplate
,
302 stationInfoSrc
: ChargingStationInfo
,
303 stationInfoDst
: ChargingStationInfo
305 if (!stationInfoSrc
|| !stationTemplate
) {
307 'Missing charging station template or existing configuration to propagate serial number'
310 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
311 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
312 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
313 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
314 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
315 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
316 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
317 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
318 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
321 public static getAmperageLimitationUnitDivider(stationInfo
: ChargingStationInfo
): number {
323 switch (stationInfo
.amperageLimitationUnit
) {
324 case AmpereUnits
.DECI_AMPERE
:
327 case AmpereUnits
.CENTI_AMPERE
:
330 case AmpereUnits
.MILLI_AMPERE
:
337 public static getChargingStationConnectorChargingProfilesPowerLimit(
338 chargingStation
: ChargingStation
,
340 ): number | undefined {
341 let limit
: number, matchingChargingProfile
: ChargingProfile
;
342 // Get charging profiles for connector and sort by stack level
343 const chargingProfiles
=
344 Utils
.cloneObject(chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
)?.sort(
345 (a
, b
) => b
.stackLevel
- a
.stackLevel
347 // Get profiles on connector 0
348 if (chargingStation
.getConnectorStatus(0)?.chargingProfiles
) {
349 chargingProfiles
.push(
350 ...Utils
.cloneObject(chargingStation
.getConnectorStatus(0).chargingProfiles
).sort(
351 (a
, b
) => b
.stackLevel
- a
.stackLevel
355 if (Utils
.isNotEmptyArray(chargingProfiles
)) {
356 const result
= ChargingStationUtils
.getLimitFromChargingProfiles(
358 chargingStation
.logPrefix()
360 if (!Utils
.isNullOrUndefined(result
)) {
361 limit
= result
?.limit
;
362 matchingChargingProfile
= result
?.matchingChargingProfile
;
363 switch (chargingStation
.getCurrentOutType()) {
366 matchingChargingProfile
.chargingSchedule
.chargingRateUnit
===
367 ChargingRateUnitType
.WATT
369 : ACElectricUtils
.powerTotal(
370 chargingStation
.getNumberOfPhases(),
371 chargingStation
.getVoltageOut(),
377 matchingChargingProfile
.chargingSchedule
.chargingRateUnit
===
378 ChargingRateUnitType
.WATT
380 : DCElectricUtils
.power(chargingStation
.getVoltageOut(), limit
);
382 const connectorMaximumPower
=
383 chargingStation
.getMaximumPower() / chargingStation
.powerDivider
;
384 if (limit
> connectorMaximumPower
) {
386 `${chargingStation.logPrefix()} Charging profile id ${
387 matchingChargingProfile.chargingProfileId
388 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
391 limit
= connectorMaximumPower
;
398 public static getDefaultVoltageOut(
399 currentType
: CurrentType
,
400 templateFile
: string,
403 const errMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
404 let defaultVoltageOut
: number;
405 switch (currentType
) {
407 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
410 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
413 logger
.error(`${logPrefix} ${errMsg}`);
414 throw new BaseError(errMsg
);
416 return defaultVoltageOut
;
419 public static getIdTagsFile(stationInfo
: ChargingStationInfo
): string | undefined {
421 stationInfo
.idTagsFile
&&
423 path
.resolve(path
.dirname(fileURLToPath(import.meta
.url
)), '../'),
425 path
.basename(stationInfo
.idTagsFile
)
430 private static warnDeprecatedTemplateKey(
431 template
: ChargingStationTemplate
,
433 templateFile
: string,
437 if (!Utils
.isUndefined(template
[key
])) {
438 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
439 Utils.isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
441 logger
.warn(`${logPrefix} ${logMsg}`);
442 console
.warn(chalk
.yellow(`${logMsg}`));
446 private static convertDeprecatedTemplateKey(
447 template
: ChargingStationTemplate
,
448 deprecatedKey
: string,
451 if (!Utils
.isUndefined(template
[deprecatedKey
])) {
452 template
[key
] = template
[deprecatedKey
] as unknown
;
453 delete template
[deprecatedKey
];
458 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
460 * @param chargingProfiles -
464 private static getLimitFromChargingProfiles(
465 chargingProfiles
: ChargingProfile
[],
469 matchingChargingProfile
: ChargingProfile
;
471 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
472 const currentMoment
= moment();
473 const currentDate
= new Date();
474 for (const chargingProfile
of chargingProfiles
) {
476 const chargingSchedule
= chargingProfile
.chargingSchedule
;
477 if (!chargingSchedule
?.startSchedule
) {
479 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}`
482 // Check type (recurring) and if it is already active
483 // Adjust the daily recurring schedule to today
485 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
486 chargingProfile
.recurrencyKind
=== RecurrencyKindType
.DAILY
&&
487 currentMoment
.isAfter(chargingSchedule
.startSchedule
)
489 if (!(chargingSchedule
?.startSchedule
instanceof Date)) {
491 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not a Date object in charging profile id ${chargingProfile.chargingProfileId}. Trying to convert it to a Date object`
493 chargingSchedule
.startSchedule
= new Date(chargingSchedule
.startSchedule
);
495 chargingSchedule
.startSchedule
.setFullYear(
496 currentDate
.getFullYear(),
497 currentDate
.getMonth(),
498 currentDate
.getDate()
500 // Check if the start of the schedule is yesterday
501 if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
502 chargingSchedule
.startSchedule
.setDate(currentDate
.getDate() - 1);
504 } else if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
507 // Check if the charging profile is active
509 moment(chargingSchedule
.startSchedule
)
510 .add(chargingSchedule
.duration
, 's')
511 .isAfter(currentMoment
)
513 let lastButOneSchedule
: ChargingSchedulePeriod
;
514 // Search the right schedule period
515 for (const schedulePeriod
of chargingSchedule
.chargingSchedulePeriod
) {
516 // Handling of only one period
518 chargingSchedule
.chargingSchedulePeriod
.length
=== 1 &&
519 schedulePeriod
.startPeriod
=== 0
522 limit
: schedulePeriod
.limit
,
523 matchingChargingProfile
: chargingProfile
,
525 logger
.debug(debugLogMsg
, result
);
528 // Find the right schedule period
530 moment(chargingSchedule
.startSchedule
)
531 .add(schedulePeriod
.startPeriod
, 's')
532 .isAfter(currentMoment
)
534 // Found the schedule: last but one is the correct one
536 limit
: lastButOneSchedule
.limit
,
537 matchingChargingProfile
: chargingProfile
,
539 logger
.debug(debugLogMsg
, result
);
543 lastButOneSchedule
= schedulePeriod
;
544 // Handle the last schedule period
546 schedulePeriod
.startPeriod
===
547 chargingSchedule
.chargingSchedulePeriod
[
548 chargingSchedule
.chargingSchedulePeriod
.length
- 1
552 limit
: lastButOneSchedule
.limit
,
553 matchingChargingProfile
: chargingProfile
,
555 logger
.debug(debugLogMsg
, result
);
564 private static getRandomSerialNumberSuffix(params
?: {
565 randomBytesLength
?: number;
568 const randomSerialNumberSuffix
= crypto
569 .randomBytes(params
?.randomBytesLength
?? 16)
571 if (params
?.upperCase
) {
572 return randomSerialNumberSuffix
.toUpperCase();
574 return randomSerialNumberSuffix
;