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 getTemplateMaxNumberOfConnectors(stationTemplate
: ChargingStationTemplate
): number {
93 const templateConnectors
= stationTemplate
?.Connectors
;
94 if (!templateConnectors
) {
97 return Object.keys(templateConnectors
).length
;
100 public static checkTemplateMaxConnectors(
101 templateMaxConnectors
: number,
102 templateFile
: string,
105 if (templateMaxConnectors
=== 0) {
107 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
109 } else if (templateMaxConnectors
< 0) {
111 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
116 public static getConfiguredNumberOfConnectors(stationTemplate
: ChargingStationTemplate
): number {
117 let configuredMaxConnectors
: number;
118 if (Utils
.isNotEmptyArray(stationTemplate
.numberOfConnectors
) === true) {
119 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
120 configuredMaxConnectors
=
121 numberOfConnectors
[Math.floor(Utils
.secureRandom() * numberOfConnectors
.length
)];
122 } else if (Utils
.isUndefined(stationTemplate
.numberOfConnectors
) === false) {
123 configuredMaxConnectors
= stationTemplate
.numberOfConnectors
as number;
125 configuredMaxConnectors
= stationTemplate
?.Connectors
[0]
126 ? ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
) - 1
127 : ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
);
129 return configuredMaxConnectors
;
132 public static checkConfiguredMaxConnectors(
133 configuredMaxConnectors
: number,
134 templateFile
: string,
137 if (configuredMaxConnectors
<= 0) {
139 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
144 public static createBootNotificationRequest(
145 stationInfo
: ChargingStationInfo
,
146 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
147 ): BootNotificationRequest
{
148 const ocppVersion
= stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
;
149 switch (ocppVersion
) {
150 case OCPPVersion
.VERSION_16
:
152 chargePointModel
: stationInfo
.chargePointModel
,
153 chargePointVendor
: stationInfo
.chargePointVendor
,
154 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
155 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
157 ...(!Utils
.isUndefined(stationInfo
.chargePointSerialNumber
) && {
158 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
160 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
161 firmwareVersion
: stationInfo
.firmwareVersion
,
163 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
164 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
165 ...(!Utils
.isUndefined(stationInfo
.meterSerialNumber
) && {
166 meterSerialNumber
: stationInfo
.meterSerialNumber
,
168 ...(!Utils
.isUndefined(stationInfo
.meterType
) && {
169 meterType
: stationInfo
.meterType
,
171 } as OCPP16BootNotificationRequest
;
172 case OCPPVersion
.VERSION_20
:
173 case OCPPVersion
.VERSION_201
:
177 model
: stationInfo
.chargePointModel
,
178 vendorName
: stationInfo
.chargePointVendor
,
179 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
180 firmwareVersion
: stationInfo
.firmwareVersion
,
182 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
183 serialNumber
: stationInfo
.chargeBoxSerialNumber
,
185 ...((!Utils
.isUndefined(stationInfo
.iccid
) || !Utils
.isUndefined(stationInfo
.imsi
)) && {
187 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
188 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
192 } as OCPP20BootNotificationRequest
;
196 public static workerPoolInUse(): boolean {
197 return [WorkerProcessType
.DYNAMIC_POOL
, WorkerProcessType
.STATIC_POOL
].includes(
198 Configuration
.getWorker().processType
202 public static workerDynamicPoolInUse(): boolean {
203 return Configuration
.getWorker().processType
=== WorkerProcessType
.DYNAMIC_POOL
;
206 public static warnDeprecatedTemplateKey(
207 template
: ChargingStationTemplate
,
209 templateFile
: string,
213 if (!Utils
.isUndefined(template
[key
])) {
215 `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
216 Utils.isNotEmptyString(logMsgToAppend) && `. ${logMsgToAppend}
`
222 public static convertDeprecatedTemplateKey(
223 template
: ChargingStationTemplate
,
224 deprecatedKey
: string,
227 if (!Utils
.isUndefined(template
[deprecatedKey
])) {
228 template
[key
] = template
[deprecatedKey
] as unknown
;
229 delete template
[deprecatedKey
];
233 public static stationTemplateToStationInfo(
234 stationTemplate
: ChargingStationTemplate
235 ): ChargingStationInfo
{
236 stationTemplate
= Utils
.cloneObject(stationTemplate
);
237 delete stationTemplate
.power
;
238 delete stationTemplate
.powerUnit
;
239 delete stationTemplate
.Configuration
;
240 delete stationTemplate
.AutomaticTransactionGenerator
;
241 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
242 delete stationTemplate
.chargePointSerialNumberPrefix
;
243 delete stationTemplate
.meterSerialNumberPrefix
;
244 return stationTemplate
as unknown
as ChargingStationInfo
;
247 public static createStationInfoHash(stationInfo
: ChargingStationInfo
): void {
248 delete stationInfo
.infoHash
;
249 stationInfo
.infoHash
= crypto
250 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
251 .update(JSON
.stringify(stationInfo
))
255 public static createSerialNumber(
256 stationTemplate
: ChargingStationTemplate
,
257 stationInfo
: ChargingStationInfo
,
259 randomSerialNumberUpperCase
?: boolean;
260 randomSerialNumber
?: boolean;
262 randomSerialNumberUpperCase
: true,
263 randomSerialNumber
: true,
266 params
= params
?? {};
267 params
.randomSerialNumberUpperCase
= params
?.randomSerialNumberUpperCase
?? true;
268 params
.randomSerialNumber
= params
?.randomSerialNumber
?? true;
269 const serialNumberSuffix
= params
?.randomSerialNumber
270 ? ChargingStationUtils
.getRandomSerialNumberSuffix({
271 upperCase
: params
.randomSerialNumberUpperCase
,
274 stationInfo
.chargePointSerialNumber
= Utils
.isNotEmptyString(
275 stationTemplate
?.chargePointSerialNumberPrefix
277 ? `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`
279 stationInfo
.chargeBoxSerialNumber
= Utils
.isNotEmptyString(
280 stationTemplate
?.chargeBoxSerialNumberPrefix
282 ? `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`
284 stationInfo
.meterSerialNumber
= Utils
.isNotEmptyString(stationTemplate
?.meterSerialNumberPrefix
)
285 ? `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`
289 public static propagateSerialNumber(
290 stationTemplate
: ChargingStationTemplate
,
291 stationInfoSrc
: ChargingStationInfo
,
292 stationInfoDst
: ChargingStationInfo
294 if (!stationInfoSrc
|| !stationTemplate
) {
296 'Missing charging station template or existing configuration to propagate serial number'
299 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
300 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
301 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
302 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
303 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
304 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
305 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
306 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
307 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
310 public static getAmperageLimitationUnitDivider(stationInfo
: ChargingStationInfo
): number {
312 switch (stationInfo
.amperageLimitationUnit
) {
313 case AmpereUnits
.DECI_AMPERE
:
316 case AmpereUnits
.CENTI_AMPERE
:
319 case AmpereUnits
.MILLI_AMPERE
:
326 public static getChargingStationConnectorChargingProfilesPowerLimit(
327 chargingStation
: ChargingStation
,
329 ): number | undefined {
330 let limit
: number, matchingChargingProfile
: ChargingProfile
;
331 let chargingProfiles
: ChargingProfile
[] = [];
332 // Get charging profiles for connector and sort by stack level
333 chargingProfiles
= chargingStation
334 .getConnectorStatus(connectorId
)
335 ?.chargingProfiles
?.sort((a
, b
) => b
.stackLevel
- a
.stackLevel
);
336 // Get profiles on connector 0
337 if (chargingStation
.getConnectorStatus(0)?.chargingProfiles
) {
338 chargingProfiles
.push(
340 .getConnectorStatus(0)
341 .chargingProfiles
.sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
344 if (Utils
.isNotEmptyArray(chargingProfiles
)) {
345 const result
= ChargingStationUtils
.getLimitFromChargingProfiles(
347 chargingStation
.logPrefix()
349 if (!Utils
.isNullOrUndefined(result
)) {
350 limit
= result
?.limit
;
351 matchingChargingProfile
= result
?.matchingChargingProfile
;
352 switch (chargingStation
.getCurrentOutType()) {
355 matchingChargingProfile
.chargingSchedule
.chargingRateUnit
===
356 ChargingRateUnitType
.WATT
358 : ACElectricUtils
.powerTotal(
359 chargingStation
.getNumberOfPhases(),
360 chargingStation
.getVoltageOut(),
366 matchingChargingProfile
.chargingSchedule
.chargingRateUnit
===
367 ChargingRateUnitType
.WATT
369 : DCElectricUtils
.power(chargingStation
.getVoltageOut(), limit
);
371 const connectorMaximumPower
=
372 chargingStation
.getMaximumPower() / chargingStation
.powerDivider
;
373 if (limit
> connectorMaximumPower
) {
375 `${chargingStation.logPrefix()} Charging profile id ${
376 matchingChargingProfile.chargingProfileId
377 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
380 limit
= connectorMaximumPower
;
387 public static getDefaultVoltageOut(
388 currentType
: CurrentType
,
389 templateFile
: string,
392 const errMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
393 let defaultVoltageOut
: number;
394 switch (currentType
) {
396 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
399 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
402 logger
.error(`${logPrefix} ${errMsg}`);
403 throw new BaseError(errMsg
);
405 return defaultVoltageOut
;
408 public static getAuthorizationFile(stationInfo
: ChargingStationInfo
): string | undefined {
410 stationInfo
.authorizationFile
&&
412 path
.resolve(path
.dirname(fileURLToPath(import.meta
.url
)), '../'),
414 path
.basename(stationInfo
.authorizationFile
)
420 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
422 * @param chargingProfiles -
426 private static getLimitFromChargingProfiles(
427 chargingProfiles
: ChargingProfile
[],
431 matchingChargingProfile
: ChargingProfile
;
433 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
434 for (const chargingProfile
of chargingProfiles
) {
436 const currentMoment
= moment();
437 const chargingSchedule
= chargingProfile
.chargingSchedule
;
438 // Check type (recurring) and if it is already active
439 // Adjust the daily recurring schedule to today
441 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
442 chargingProfile
.recurrencyKind
=== RecurrencyKindType
.DAILY
&&
443 currentMoment
.isAfter(chargingSchedule
.startSchedule
)
445 const currentDate
= new Date();
446 chargingSchedule
.startSchedule
= new Date(chargingSchedule
.startSchedule
);
447 chargingSchedule
.startSchedule
.setFullYear(
448 currentDate
.getFullYear(),
449 currentDate
.getMonth(),
450 currentDate
.getDate()
452 // Check if the start of the schedule is yesterday
453 if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
454 chargingSchedule
.startSchedule
.setDate(currentDate
.getDate() - 1);
456 } else if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
459 // Check if the charging profile is active
461 moment(chargingSchedule
.startSchedule
)
462 .add(chargingSchedule
.duration
, 's')
463 .isAfter(currentMoment
)
465 let lastButOneSchedule
: ChargingSchedulePeriod
;
466 // Search the right schedule period
467 for (const schedulePeriod
of chargingSchedule
.chargingSchedulePeriod
) {
468 // Handling of only one period
470 chargingSchedule
.chargingSchedulePeriod
.length
=== 1 &&
471 schedulePeriod
.startPeriod
=== 0
474 limit
: schedulePeriod
.limit
,
475 matchingChargingProfile
: chargingProfile
,
477 logger
.debug(debugLogMsg
, result
);
480 // Find the right schedule period
482 moment(chargingSchedule
.startSchedule
)
483 .add(schedulePeriod
.startPeriod
, 's')
484 .isAfter(currentMoment
)
486 // Found the schedule: last but one is the correct one
488 limit
: lastButOneSchedule
.limit
,
489 matchingChargingProfile
: chargingProfile
,
491 logger
.debug(debugLogMsg
, result
);
495 lastButOneSchedule
= schedulePeriod
;
496 // Handle the last schedule period
498 schedulePeriod
.startPeriod
===
499 chargingSchedule
.chargingSchedulePeriod
[
500 chargingSchedule
.chargingSchedulePeriod
.length
- 1
504 limit
: lastButOneSchedule
.limit
,
505 matchingChargingProfile
: chargingProfile
,
507 logger
.debug(debugLogMsg
, result
);
516 private static getRandomSerialNumberSuffix(params
?: {
517 randomBytesLength
?: number;
520 const randomSerialNumberSuffix
= crypto
521 .randomBytes(params
?.randomBytesLength
?? 16)
523 if (params
?.upperCase
) {
524 return randomSerialNumberSuffix
.toUpperCase();
526 return randomSerialNumberSuffix
;