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
,
21 type OCPP16BootNotificationRequest
,
22 type OCPP20BootNotificationRequest
,
35 import { WorkerProcessType
} from
'../worker';
37 const moduleName
= 'ChargingStationUtils';
39 export class ChargingStationUtils
{
40 private constructor() {
41 // This is intentional
44 public static getChargingStationId(
46 stationTemplate
: ChargingStationTemplate
48 // In case of multiple instances: add instance index to charging station id
49 const instanceIndex
= process
.env
.CF_INSTANCE_INDEX
?? 0;
50 const idSuffix
= stationTemplate
?.nameSuffix
?? '';
51 const idStr
= `000000000${index.toString()}`;
52 return stationTemplate
?.fixedName
53 ? stationTemplate
.baseName
54 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
59 public static getHashId(index
: number, stationTemplate
: ChargingStationTemplate
): string {
60 const chargingStationInfo
= {
61 chargePointModel
: stationTemplate
.chargePointModel
,
62 chargePointVendor
: stationTemplate
.chargePointVendor
,
63 ...(!Utils
.isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
64 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
66 ...(!Utils
.isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
67 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
69 ...(!Utils
.isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
70 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
72 ...(!Utils
.isUndefined(stationTemplate
.meterType
) && {
73 meterType
: stationTemplate
.meterType
,
77 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
79 `${JSON.stringify(chargingStationInfo)}${ChargingStationUtils.getChargingStationId(
87 public static checkChargingStation(chargingStation
: ChargingStation
, logPrefix
: string): boolean {
88 if (chargingStation
.started
=== false && chargingStation
.starting
=== false) {
89 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`);
95 public static getTemplateMaxNumberOfConnectors(stationTemplate
: ChargingStationTemplate
): number {
96 const templateConnectors
= stationTemplate
?.Connectors
;
97 if (!templateConnectors
) {
100 return Object.keys(templateConnectors
).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;
128 configuredMaxConnectors
= stationTemplate
?.Connectors
[0]
129 ? ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
) - 1
130 : ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
);
132 return configuredMaxConnectors
;
135 public static checkConfiguredMaxConnectors(
136 configuredMaxConnectors
: number,
137 templateFile
: string,
140 if (configuredMaxConnectors
<= 0) {
142 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
147 public static createBootNotificationRequest(
148 stationInfo
: ChargingStationInfo
,
149 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
150 ): BootNotificationRequest
{
151 const ocppVersion
= stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
;
152 switch (ocppVersion
) {
153 case OCPPVersion
.VERSION_16
:
155 chargePointModel
: stationInfo
.chargePointModel
,
156 chargePointVendor
: stationInfo
.chargePointVendor
,
157 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
158 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
160 ...(!Utils
.isUndefined(stationInfo
.chargePointSerialNumber
) && {
161 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
163 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
164 firmwareVersion
: stationInfo
.firmwareVersion
,
166 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
167 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
168 ...(!Utils
.isUndefined(stationInfo
.meterSerialNumber
) && {
169 meterSerialNumber
: stationInfo
.meterSerialNumber
,
171 ...(!Utils
.isUndefined(stationInfo
.meterType
) && {
172 meterType
: stationInfo
.meterType
,
174 } as OCPP16BootNotificationRequest
;
175 case OCPPVersion
.VERSION_20
:
176 case OCPPVersion
.VERSION_201
:
180 model
: stationInfo
.chargePointModel
,
181 vendorName
: stationInfo
.chargePointVendor
,
182 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
183 firmwareVersion
: stationInfo
.firmwareVersion
,
185 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
186 serialNumber
: stationInfo
.chargeBoxSerialNumber
,
188 ...((!Utils
.isUndefined(stationInfo
.iccid
) || !Utils
.isUndefined(stationInfo
.imsi
)) && {
190 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
191 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
195 } as OCPP20BootNotificationRequest
;
199 public static workerPoolInUse(): boolean {
200 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.staticPool
].includes(
201 Configuration
.getWorker().processType
205 public static workerDynamicPoolInUse(): boolean {
206 return Configuration
.getWorker().processType
=== WorkerProcessType
.dynamicPool
;
209 public static warnTemplateKeysDeprecation(
210 templateFile
: string,
211 stationTemplate
: ChargingStationTemplate
,
214 const templateKeys
: { key
: string; deprecatedKey
: string }[] = [
215 { key
: 'supervisionUrls', deprecatedKey
: 'supervisionUrl' },
216 { key
: 'idTagsFile', deprecatedKey
: 'authorizationFile' },
218 for (const templateKey
of templateKeys
) {
219 ChargingStationUtils
.warnDeprecatedTemplateKey(
221 templateKey
.deprecatedKey
,
224 `Use '${templateKey.key}' instead`
226 ChargingStationUtils
.convertDeprecatedTemplateKey(
228 templateKey
.deprecatedKey
,
234 public static stationTemplateToStationInfo(
235 stationTemplate
: ChargingStationTemplate
236 ): ChargingStationInfo
{
237 stationTemplate
= Utils
.cloneObject(stationTemplate
);
238 delete stationTemplate
.power
;
239 delete stationTemplate
.powerUnit
;
240 delete stationTemplate
.Configuration
;
241 delete stationTemplate
.AutomaticTransactionGenerator
;
242 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
243 delete stationTemplate
.chargePointSerialNumberPrefix
;
244 delete stationTemplate
.meterSerialNumberPrefix
;
245 return stationTemplate
as unknown
as ChargingStationInfo
;
248 public static createStationInfoHash(stationInfo
: ChargingStationInfo
): void {
249 delete stationInfo
.infoHash
;
250 stationInfo
.infoHash
= crypto
251 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
252 .update(JSON
.stringify(stationInfo
))
256 public static createSerialNumber(
257 stationTemplate
: ChargingStationTemplate
,
258 stationInfo
: ChargingStationInfo
,
260 randomSerialNumberUpperCase
?: boolean;
261 randomSerialNumber
?: boolean;
263 randomSerialNumberUpperCase
: true,
264 randomSerialNumber
: true,
267 params
= params
?? {};
268 params
.randomSerialNumberUpperCase
= params
?.randomSerialNumberUpperCase
?? true;
269 params
.randomSerialNumber
= params
?.randomSerialNumber
?? true;
270 const serialNumberSuffix
= params
?.randomSerialNumber
271 ? ChargingStationUtils
.getRandomSerialNumberSuffix({
272 upperCase
: params
.randomSerialNumberUpperCase
,
275 stationInfo
.chargePointSerialNumber
= Utils
.isNotEmptyString(
276 stationTemplate
?.chargePointSerialNumberPrefix
278 ? `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`
280 stationInfo
.chargeBoxSerialNumber
= Utils
.isNotEmptyString(
281 stationTemplate
?.chargeBoxSerialNumberPrefix
283 ? `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`
285 stationInfo
.meterSerialNumber
= Utils
.isNotEmptyString(stationTemplate
?.meterSerialNumberPrefix
)
286 ? `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`
290 public static propagateSerialNumber(
291 stationTemplate
: ChargingStationTemplate
,
292 stationInfoSrc
: ChargingStationInfo
,
293 stationInfoDst
: ChargingStationInfo
295 if (!stationInfoSrc
|| !stationTemplate
) {
297 'Missing charging station template or existing configuration to propagate serial number'
300 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
301 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
302 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
303 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
304 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
305 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
306 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
307 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
308 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
311 public static getAmperageLimitationUnitDivider(stationInfo
: ChargingStationInfo
): number {
313 switch (stationInfo
.amperageLimitationUnit
) {
314 case AmpereUnits
.DECI_AMPERE
:
317 case AmpereUnits
.CENTI_AMPERE
:
320 case AmpereUnits
.MILLI_AMPERE
:
327 public static getChargingStationConnectorChargingProfilesPowerLimit(
328 chargingStation
: ChargingStation
,
330 ): number | undefined {
331 let limit
: number, matchingChargingProfile
: ChargingProfile
;
332 // Get charging profiles for connector and sort by stack level
333 const chargingProfiles
= Utils
.cloneObject(
335 .getConnectorStatus(connectorId
)
336 ?.chargingProfiles
?.sort((a
, b
) => b
.stackLevel
- a
.stackLevel
) ?? []
338 // Get profiles on connector 0
339 if (chargingStation
.getConnectorStatus(0)?.chargingProfiles
) {
340 chargingProfiles
.push(
342 .getConnectorStatus(0)
343 .chargingProfiles
.sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
346 if (Utils
.isNotEmptyArray(chargingProfiles
)) {
347 const result
= ChargingStationUtils
.getLimitFromChargingProfiles(
349 chargingStation
.logPrefix()
351 if (!Utils
.isNullOrUndefined(result
)) {
352 limit
= result
?.limit
;
353 matchingChargingProfile
= result
?.matchingChargingProfile
;
354 switch (chargingStation
.getCurrentOutType()) {
357 matchingChargingProfile
.chargingSchedule
.chargingRateUnit
===
358 ChargingRateUnitType
.WATT
360 : ACElectricUtils
.powerTotal(
361 chargingStation
.getNumberOfPhases(),
362 chargingStation
.getVoltageOut(),
368 matchingChargingProfile
.chargingSchedule
.chargingRateUnit
===
369 ChargingRateUnitType
.WATT
371 : DCElectricUtils
.power(chargingStation
.getVoltageOut(), limit
);
373 const connectorMaximumPower
=
374 chargingStation
.getMaximumPower() / chargingStation
.powerDivider
;
375 if (limit
> connectorMaximumPower
) {
377 `${chargingStation.logPrefix()} Charging profile id ${
378 matchingChargingProfile.chargingProfileId
379 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
382 limit
= connectorMaximumPower
;
389 public static getDefaultVoltageOut(
390 currentType
: CurrentType
,
391 templateFile
: string,
394 const errMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
395 let defaultVoltageOut
: number;
396 switch (currentType
) {
398 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
401 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
404 logger
.error(`${logPrefix} ${errMsg}`);
405 throw new BaseError(errMsg
);
407 return defaultVoltageOut
;
410 public static getIdTagsFile(stationInfo
: ChargingStationInfo
): string | undefined {
412 stationInfo
.idTagsFile
&&
414 path
.resolve(path
.dirname(fileURLToPath(import.meta
.url
)), '../'),
416 path
.basename(stationInfo
.idTagsFile
)
421 private static warnDeprecatedTemplateKey(
422 template
: ChargingStationTemplate
,
424 templateFile
: string,
428 if (!Utils
.isUndefined(template
[key
])) {
429 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
430 Utils.isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
432 logger
.warn(`${logPrefix} ${logMsg}`);
433 console
.warn(chalk
.yellow(`${logMsg}`));
437 private static convertDeprecatedTemplateKey(
438 template
: ChargingStationTemplate
,
439 deprecatedKey
: string,
442 if (!Utils
.isUndefined(template
[deprecatedKey
])) {
443 template
[key
] = template
[deprecatedKey
] as unknown
;
444 delete template
[deprecatedKey
];
449 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
451 * @param chargingProfiles -
455 private static getLimitFromChargingProfiles(
456 chargingProfiles
: ChargingProfile
[],
460 matchingChargingProfile
: ChargingProfile
;
462 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
463 const currentMoment
= moment();
464 const currentDate
= new Date();
465 for (const chargingProfile
of chargingProfiles
) {
467 const chargingSchedule
= chargingProfile
.chargingSchedule
;
468 if (!chargingSchedule
?.startSchedule
) {
470 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}`
473 // Check type (recurring) and if it is already active
474 // Adjust the daily recurring schedule to today
476 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
477 chargingProfile
.recurrencyKind
=== RecurrencyKindType
.DAILY
&&
478 currentMoment
.isAfter(chargingSchedule
.startSchedule
)
480 if (!(chargingSchedule
?.startSchedule
instanceof Date)) {
482 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not a Date object in charging profile id ${chargingProfile.chargingProfileId}. Trying to convert it to a Date object`
484 chargingSchedule
.startSchedule
= new Date(chargingSchedule
.startSchedule
);
486 chargingSchedule
.startSchedule
.setFullYear(
487 currentDate
.getFullYear(),
488 currentDate
.getMonth(),
489 currentDate
.getDate()
491 // Check if the start of the schedule is yesterday
492 if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
493 chargingSchedule
.startSchedule
.setDate(currentDate
.getDate() - 1);
495 } else if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
498 // Check if the charging profile is active
500 moment(chargingSchedule
.startSchedule
)
501 .add(chargingSchedule
.duration
, 's')
502 .isAfter(currentMoment
)
504 let lastButOneSchedule
: ChargingSchedulePeriod
;
505 // Search the right schedule period
506 for (const schedulePeriod
of chargingSchedule
.chargingSchedulePeriod
) {
507 // Handling of only one period
509 chargingSchedule
.chargingSchedulePeriod
.length
=== 1 &&
510 schedulePeriod
.startPeriod
=== 0
513 limit
: schedulePeriod
.limit
,
514 matchingChargingProfile
: chargingProfile
,
516 logger
.debug(debugLogMsg
, result
);
519 // Find the right schedule period
521 moment(chargingSchedule
.startSchedule
)
522 .add(schedulePeriod
.startPeriod
, 's')
523 .isAfter(currentMoment
)
525 // Found the schedule: last but one is the correct one
527 limit
: lastButOneSchedule
.limit
,
528 matchingChargingProfile
: chargingProfile
,
530 logger
.debug(debugLogMsg
, result
);
534 lastButOneSchedule
= schedulePeriod
;
535 // Handle the last schedule period
537 schedulePeriod
.startPeriod
===
538 chargingSchedule
.chargingSchedulePeriod
[
539 chargingSchedule
.chargingSchedulePeriod
.length
- 1
543 limit
: lastButOneSchedule
.limit
,
544 matchingChargingProfile
: chargingProfile
,
546 logger
.debug(debugLogMsg
, result
);
555 private static getRandomSerialNumberSuffix(params
?: {
556 randomBytesLength
?: number;
559 const randomSerialNumberSuffix
= crypto
560 .randomBytes(params
?.randomBytesLength
?? 16)
562 if (params
?.upperCase
) {
563 return randomSerialNumberSuffix
.toUpperCase();
565 return randomSerialNumberSuffix
;