1 import crypto from
'node:crypto';
2 import path from
'node:path';
3 import { fileURLToPath
} from
'node:url';
5 import moment from
'moment';
7 import type { ChargingStation
} from
'./internal';
8 import { BaseError
} from
'../exception';
11 type BootNotificationRequest
,
14 ChargingProfileKindType
,
16 type ChargingSchedulePeriod
,
17 type ChargingStationInfo
,
18 type ChargingStationTemplate
,
20 type OCPP16BootNotificationRequest
,
21 type OCPP20BootNotificationRequest
,
34 import { WorkerProcessType
} from
'../worker';
36 const moduleName
= 'ChargingStationUtils';
38 export class ChargingStationUtils
{
39 private constructor() {
40 // This is intentional
43 public static getChargingStationId(
45 stationTemplate
: ChargingStationTemplate
47 // In case of multiple instances: add instance index to charging station id
48 const instanceIndex
= process
.env
.CF_INSTANCE_INDEX
?? 0;
49 const idSuffix
= stationTemplate
?.nameSuffix
?? '';
50 const idStr
= `000000000${index.toString()}`;
51 return stationTemplate
?.fixedName
52 ? stationTemplate
.baseName
53 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
58 public static getHashId(index
: number, stationTemplate
: ChargingStationTemplate
): string {
59 const chargingStationInfo
= {
60 chargePointModel
: stationTemplate
.chargePointModel
,
61 chargePointVendor
: stationTemplate
.chargePointVendor
,
62 ...(!Utils
.isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
63 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
65 ...(!Utils
.isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
66 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
68 // FIXME?: Should a firmware version change always reference a new configuration file?
69 ...(!Utils
.isUndefined(stationTemplate
.firmwareVersion
) && {
70 firmwareVersion
: stationTemplate
.firmwareVersion
,
72 ...(!Utils
.isUndefined(stationTemplate
.iccid
) && { iccid
: stationTemplate
.iccid
}),
73 ...(!Utils
.isUndefined(stationTemplate
.imsi
) && { imsi
: stationTemplate
.imsi
}),
74 ...(!Utils
.isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
75 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
77 ...(!Utils
.isUndefined(stationTemplate
.meterType
) && {
78 meterType
: stationTemplate
.meterType
,
82 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
84 `${JSON.stringify(chargingStationInfo)}${ChargingStationUtils.getChargingStationId(
92 public static checkChargingStation(chargingStation
: ChargingStation
, logPrefix
: string): boolean {
93 if (chargingStation
.started
=== false && chargingStation
.starting
=== false) {
94 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`);
100 public static getTemplateMaxNumberOfConnectors(stationTemplate
: ChargingStationTemplate
): number {
101 const templateConnectors
= stationTemplate
?.Connectors
;
102 if (!templateConnectors
) {
105 return Object.keys(templateConnectors
).length
;
108 public static checkTemplateMaxConnectors(
109 templateMaxConnectors
: number,
110 templateFile
: string,
113 if (templateMaxConnectors
=== 0) {
115 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
117 } else if (templateMaxConnectors
< 0) {
119 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
124 public static getConfiguredNumberOfConnectors(stationTemplate
: ChargingStationTemplate
): number {
125 let configuredMaxConnectors
: number;
126 if (Utils
.isNotEmptyArray(stationTemplate
.numberOfConnectors
) === true) {
127 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
128 configuredMaxConnectors
=
129 numberOfConnectors
[Math.floor(Utils
.secureRandom() * numberOfConnectors
.length
)];
130 } else if (Utils
.isUndefined(stationTemplate
.numberOfConnectors
) === false) {
131 configuredMaxConnectors
= stationTemplate
.numberOfConnectors
as number;
133 configuredMaxConnectors
= stationTemplate
?.Connectors
[0]
134 ? ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
) - 1
135 : ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
);
137 return configuredMaxConnectors
;
140 public static checkConfiguredMaxConnectors(
141 configuredMaxConnectors
: number,
142 templateFile
: string,
145 if (configuredMaxConnectors
<= 0) {
147 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
152 public static createBootNotificationRequest(
153 stationInfo
: ChargingStationInfo
,
154 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
155 ): BootNotificationRequest
{
156 const ocppVersion
= stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
;
157 switch (ocppVersion
) {
158 case OCPPVersion
.VERSION_16
:
160 chargePointModel
: stationInfo
.chargePointModel
,
161 chargePointVendor
: stationInfo
.chargePointVendor
,
162 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
163 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
165 ...(!Utils
.isUndefined(stationInfo
.chargePointSerialNumber
) && {
166 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
168 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
169 firmwareVersion
: stationInfo
.firmwareVersion
,
171 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
172 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
173 ...(!Utils
.isUndefined(stationInfo
.meterSerialNumber
) && {
174 meterSerialNumber
: stationInfo
.meterSerialNumber
,
176 ...(!Utils
.isUndefined(stationInfo
.meterType
) && {
177 meterType
: stationInfo
.meterType
,
179 } as OCPP16BootNotificationRequest
;
180 case OCPPVersion
.VERSION_20
:
181 case OCPPVersion
.VERSION_201
:
185 model
: stationInfo
.chargePointModel
,
186 vendorName
: stationInfo
.chargePointVendor
,
187 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
188 firmwareVersion
: stationInfo
.firmwareVersion
,
190 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
191 serialNumber
: stationInfo
.chargeBoxSerialNumber
,
193 ...((!Utils
.isUndefined(stationInfo
.iccid
) || !Utils
.isUndefined(stationInfo
.imsi
)) && {
195 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
196 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
200 } as OCPP20BootNotificationRequest
;
204 public static workerPoolInUse(): boolean {
205 return [WorkerProcessType
.dynamicPool
, WorkerProcessType
.staticPool
].includes(
206 Configuration
.getWorker().processType
210 public static workerDynamicPoolInUse(): boolean {
211 return Configuration
.getWorker().processType
=== WorkerProcessType
.dynamicPool
;
214 public static warnDeprecatedTemplateKey(
215 template
: ChargingStationTemplate
,
217 templateFile
: string,
221 if (!Utils
.isUndefined(template
[key
])) {
223 `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
224 Utils.isNotEmptyString(logMsgToAppend) && `. ${logMsgToAppend}
`
230 public static convertDeprecatedTemplateKey(
231 template
: ChargingStationTemplate
,
232 deprecatedKey
: string,
235 if (!Utils
.isUndefined(template
[deprecatedKey
])) {
236 template
[key
] = template
[deprecatedKey
] as unknown
;
237 delete template
[deprecatedKey
];
241 public static stationTemplateToStationInfo(
242 stationTemplate
: ChargingStationTemplate
243 ): ChargingStationInfo
{
244 stationTemplate
= Utils
.cloneObject(stationTemplate
);
245 delete stationTemplate
.power
;
246 delete stationTemplate
.powerUnit
;
247 delete stationTemplate
.Configuration
;
248 delete stationTemplate
.AutomaticTransactionGenerator
;
249 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
250 delete stationTemplate
.chargePointSerialNumberPrefix
;
251 delete stationTemplate
.meterSerialNumberPrefix
;
252 return stationTemplate
as unknown
as ChargingStationInfo
;
255 public static createStationInfoHash(stationInfo
: ChargingStationInfo
): void {
256 delete stationInfo
.infoHash
;
257 stationInfo
.infoHash
= crypto
258 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
259 .update(JSON
.stringify(stationInfo
))
263 public static createSerialNumber(
264 stationTemplate
: ChargingStationTemplate
,
265 stationInfo
: ChargingStationInfo
,
267 randomSerialNumberUpperCase
?: boolean;
268 randomSerialNumber
?: boolean;
270 randomSerialNumberUpperCase
: true,
271 randomSerialNumber
: true,
274 params
= params
?? {};
275 params
.randomSerialNumberUpperCase
= params
?.randomSerialNumberUpperCase
?? true;
276 params
.randomSerialNumber
= params
?.randomSerialNumber
?? true;
277 const serialNumberSuffix
= params
?.randomSerialNumber
278 ? ChargingStationUtils
.getRandomSerialNumberSuffix({
279 upperCase
: params
.randomSerialNumberUpperCase
,
282 stationInfo
.chargePointSerialNumber
= Utils
.isNotEmptyString(
283 stationTemplate
?.chargePointSerialNumberPrefix
285 ? `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`
287 stationInfo
.chargeBoxSerialNumber
= Utils
.isNotEmptyString(
288 stationTemplate
?.chargeBoxSerialNumberPrefix
290 ? `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`
292 stationInfo
.meterSerialNumber
= Utils
.isNotEmptyString(stationTemplate
?.meterSerialNumberPrefix
)
293 ? `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`
297 public static propagateSerialNumber(
298 stationTemplate
: ChargingStationTemplate
,
299 stationInfoSrc
: ChargingStationInfo
,
300 stationInfoDst
: ChargingStationInfo
302 if (!stationInfoSrc
|| !stationTemplate
) {
304 'Missing charging station template or existing configuration to propagate serial number'
307 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
308 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
309 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
310 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
311 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
312 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
313 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
314 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
315 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
318 public static getAmperageLimitationUnitDivider(stationInfo
: ChargingStationInfo
): number {
320 switch (stationInfo
.amperageLimitationUnit
) {
321 case AmpereUnits
.DECI_AMPERE
:
324 case AmpereUnits
.CENTI_AMPERE
:
327 case AmpereUnits
.MILLI_AMPERE
:
334 public static getChargingStationConnectorChargingProfilesPowerLimit(
335 chargingStation
: ChargingStation
,
337 ): number | undefined {
338 let limit
: number, matchingChargingProfile
: ChargingProfile
;
339 let chargingProfiles
: ChargingProfile
[] = [];
340 // Get charging profiles for connector and sort by stack level
341 chargingProfiles
= chargingStation
342 .getConnectorStatus(connectorId
)
343 ?.chargingProfiles
?.sort((a
, b
) => b
.stackLevel
- a
.stackLevel
);
344 // Get profiles on connector 0
345 if (chargingStation
.getConnectorStatus(0)?.chargingProfiles
) {
346 chargingProfiles
.push(
348 .getConnectorStatus(0)
349 .chargingProfiles
.sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
352 if (Utils
.isNotEmptyArray(chargingProfiles
)) {
353 const result
= ChargingStationUtils
.getLimitFromChargingProfiles(
355 chargingStation
.logPrefix()
357 if (!Utils
.isNullOrUndefined(result
)) {
358 limit
= result
?.limit
;
359 matchingChargingProfile
= result
?.matchingChargingProfile
;
360 switch (chargingStation
.getCurrentOutType()) {
363 matchingChargingProfile
.chargingSchedule
.chargingRateUnit
===
364 ChargingRateUnitType
.WATT
366 : ACElectricUtils
.powerTotal(
367 chargingStation
.getNumberOfPhases(),
368 chargingStation
.getVoltageOut(),
374 matchingChargingProfile
.chargingSchedule
.chargingRateUnit
===
375 ChargingRateUnitType
.WATT
377 : DCElectricUtils
.power(chargingStation
.getVoltageOut(), limit
);
379 const connectorMaximumPower
=
380 chargingStation
.getMaximumPower() / chargingStation
.powerDivider
;
381 if (limit
> connectorMaximumPower
) {
383 `${chargingStation.logPrefix()} Charging profile id ${
384 matchingChargingProfile.chargingProfileId
385 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
388 limit
= connectorMaximumPower
;
395 public static getDefaultVoltageOut(
396 currentType
: CurrentType
,
397 templateFile
: string,
400 const errMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
401 let defaultVoltageOut
: number;
402 switch (currentType
) {
404 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
407 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
410 logger
.error(`${logPrefix} ${errMsg}`);
411 throw new BaseError(errMsg
);
413 return defaultVoltageOut
;
416 public static getAuthorizationFile(stationInfo
: ChargingStationInfo
): string | undefined {
418 stationInfo
.authorizationFile
&&
420 path
.resolve(path
.dirname(fileURLToPath(import.meta
.url
)), '../'),
422 path
.basename(stationInfo
.authorizationFile
)
428 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
430 * @param chargingProfiles -
434 private static getLimitFromChargingProfiles(
435 chargingProfiles
: ChargingProfile
[],
439 matchingChargingProfile
: ChargingProfile
;
441 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
442 for (const chargingProfile
of chargingProfiles
) {
444 const currentMoment
= moment();
445 const chargingSchedule
= chargingProfile
.chargingSchedule
;
446 // Check type (recurring) and if it is already active
447 // Adjust the daily recurring schedule to today
449 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
450 chargingProfile
.recurrencyKind
=== RecurrencyKindType
.DAILY
&&
451 currentMoment
.isAfter(chargingSchedule
.startSchedule
)
453 const currentDate
= new Date();
454 chargingSchedule
.startSchedule
= new Date(chargingSchedule
.startSchedule
);
455 chargingSchedule
.startSchedule
.setFullYear(
456 currentDate
.getFullYear(),
457 currentDate
.getMonth(),
458 currentDate
.getDate()
460 // Check if the start of the schedule is yesterday
461 if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
462 chargingSchedule
.startSchedule
.setDate(currentDate
.getDate() - 1);
464 } else if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
467 // Check if the charging profile is active
469 moment(chargingSchedule
.startSchedule
)
470 .add(chargingSchedule
.duration
, 's')
471 .isAfter(currentMoment
)
473 let lastButOneSchedule
: ChargingSchedulePeriod
;
474 // Search the right schedule period
475 for (const schedulePeriod
of chargingSchedule
.chargingSchedulePeriod
) {
476 // Handling of only one period
478 chargingSchedule
.chargingSchedulePeriod
.length
=== 1 &&
479 schedulePeriod
.startPeriod
=== 0
482 limit
: schedulePeriod
.limit
,
483 matchingChargingProfile
: chargingProfile
,
485 logger
.debug(debugLogMsg
, result
);
488 // Find the right schedule period
490 moment(chargingSchedule
.startSchedule
)
491 .add(schedulePeriod
.startPeriod
, 's')
492 .isAfter(currentMoment
)
494 // Found the schedule: last but one is the correct one
496 limit
: lastButOneSchedule
.limit
,
497 matchingChargingProfile
: chargingProfile
,
499 logger
.debug(debugLogMsg
, result
);
503 lastButOneSchedule
= schedulePeriod
;
504 // Handle the last schedule period
506 schedulePeriod
.startPeriod
===
507 chargingSchedule
.chargingSchedulePeriod
[
508 chargingSchedule
.chargingSchedulePeriod
.length
- 1
512 limit
: lastButOneSchedule
.limit
,
513 matchingChargingProfile
: chargingProfile
,
515 logger
.debug(debugLogMsg
, result
);
524 private static getRandomSerialNumberSuffix(params
?: {
525 randomBytesLength
?: number;
528 const randomSerialNumberSuffix
= crypto
529 .randomBytes(params
?.randomBytesLength
?? 16)
531 if (params
?.upperCase
) {
532 return randomSerialNumberSuffix
.toUpperCase();
534 return randomSerialNumberSuffix
;