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
'./ChargingStation';
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
,
26 import { Configuration
} from
'../utils/Configuration';
27 import { Constants
} from
'../utils/Constants';
28 import { ACElectricUtils
, DCElectricUtils
} from
'../utils/ElectricUtils';
29 import { logger
} from
'../utils/Logger';
30 import { Utils
} from
'../utils/Utils';
31 import { WorkerProcessType
} from
'../worker';
33 const moduleName
= 'ChargingStationUtils';
35 export class ChargingStationUtils
{
36 private constructor() {
37 // This is intentional
40 public static getChargingStationId(
42 stationTemplate
: ChargingStationTemplate
44 // In case of multiple instances: add instance index to charging station id
45 const instanceIndex
= process
.env
.CF_INSTANCE_INDEX
?? 0;
46 const idSuffix
= stationTemplate
?.nameSuffix
?? '';
47 const idStr
= `000000000${index.toString()}`;
48 return stationTemplate
?.fixedName
49 ? stationTemplate
.baseName
50 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
55 public static getHashId(index
: number, stationTemplate
: ChargingStationTemplate
): string {
56 const chargingStationInfo
= {
57 chargePointModel
: stationTemplate
.chargePointModel
,
58 chargePointVendor
: stationTemplate
.chargePointVendor
,
59 ...(!Utils
.isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
60 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
62 ...(!Utils
.isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
63 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
65 // FIXME?: Should a firmware version change always reference a new configuration file?
66 ...(!Utils
.isUndefined(stationTemplate
.firmwareVersion
) && {
67 firmwareVersion
: stationTemplate
.firmwareVersion
,
69 ...(!Utils
.isUndefined(stationTemplate
.iccid
) && { iccid
: stationTemplate
.iccid
}),
70 ...(!Utils
.isUndefined(stationTemplate
.imsi
) && { imsi
: stationTemplate
.imsi
}),
71 ...(!Utils
.isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
72 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
74 ...(!Utils
.isUndefined(stationTemplate
.meterType
) && {
75 meterType
: stationTemplate
.meterType
,
79 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
81 `${JSON.stringify(chargingStationInfo)}${ChargingStationUtils.getChargingStationId(
89 public static getTemplateMaxNumberOfConnectors(stationTemplate
: ChargingStationTemplate
): number {
90 const templateConnectors
= stationTemplate
?.Connectors
;
91 if (!templateConnectors
) {
94 return Object.keys(templateConnectors
).length
;
97 public static checkTemplateMaxConnectors(
98 templateMaxConnectors
: number,
102 if (templateMaxConnectors
=== 0) {
104 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
106 } else if (templateMaxConnectors
< 0) {
108 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
113 public static getConfiguredNumberOfConnectors(stationTemplate
: ChargingStationTemplate
): number {
114 let configuredMaxConnectors
: number;
115 if (Utils
.isNotEmptyArray(stationTemplate
.numberOfConnectors
) === true) {
116 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
117 configuredMaxConnectors
=
118 numberOfConnectors
[Math.floor(Utils
.secureRandom() * numberOfConnectors
.length
)];
119 } else if (Utils
.isUndefined(stationTemplate
.numberOfConnectors
) === false) {
120 configuredMaxConnectors
= stationTemplate
.numberOfConnectors
as number;
122 configuredMaxConnectors
= stationTemplate
?.Connectors
[0]
123 ? ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
) - 1
124 : ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
);
126 return configuredMaxConnectors
;
129 public static checkConfiguredMaxConnectors(
130 configuredMaxConnectors
: number,
131 templateFile
: string,
134 if (configuredMaxConnectors
<= 0) {
136 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
141 public static createBootNotificationRequest(
142 stationInfo
: ChargingStationInfo
,
143 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
144 ): BootNotificationRequest
{
145 const ocppVersion
= stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
;
146 switch (ocppVersion
) {
147 case OCPPVersion
.VERSION_16
:
149 chargePointModel
: stationInfo
.chargePointModel
,
150 chargePointVendor
: stationInfo
.chargePointVendor
,
151 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
152 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
154 ...(!Utils
.isUndefined(stationInfo
.chargePointSerialNumber
) && {
155 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
157 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
158 firmwareVersion
: stationInfo
.firmwareVersion
,
160 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
161 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
162 ...(!Utils
.isUndefined(stationInfo
.meterSerialNumber
) && {
163 meterSerialNumber
: stationInfo
.meterSerialNumber
,
165 ...(!Utils
.isUndefined(stationInfo
.meterType
) && {
166 meterType
: stationInfo
.meterType
,
168 } as OCPP16BootNotificationRequest
;
169 case OCPPVersion
.VERSION_20
:
170 case OCPPVersion
.VERSION_201
:
174 model
: stationInfo
.chargePointModel
,
175 vendorName
: stationInfo
.chargePointVendor
,
176 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
177 firmwareVersion
: stationInfo
.firmwareVersion
,
179 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
180 serialNumber
: stationInfo
.chargeBoxSerialNumber
,
182 ...((!Utils
.isUndefined(stationInfo
.iccid
) || !Utils
.isUndefined(stationInfo
.imsi
)) && {
184 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
185 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
189 } as OCPP20BootNotificationRequest
;
193 public static workerPoolInUse(): boolean {
194 return [WorkerProcessType
.DYNAMIC_POOL
, WorkerProcessType
.STATIC_POOL
].includes(
195 Configuration
.getWorker().processType
199 public static workerDynamicPoolInUse(): boolean {
200 return Configuration
.getWorker().processType
=== WorkerProcessType
.DYNAMIC_POOL
;
203 public static warnDeprecatedTemplateKey(
204 template
: ChargingStationTemplate
,
206 templateFile
: string,
210 if (!Utils
.isUndefined(template
[key
])) {
212 `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
213 Utils.isNotEmptyString(logMsgToAppend) && `. ${logMsgToAppend}
`
219 public static convertDeprecatedTemplateKey(
220 template
: ChargingStationTemplate
,
221 deprecatedKey
: string,
224 if (!Utils
.isUndefined(template
[deprecatedKey
])) {
225 template
[key
] = template
[deprecatedKey
] as unknown
;
226 delete template
[deprecatedKey
];
230 public static stationTemplateToStationInfo(
231 stationTemplate
: ChargingStationTemplate
232 ): ChargingStationInfo
{
233 stationTemplate
= Utils
.cloneObject(stationTemplate
);
234 delete stationTemplate
.power
;
235 delete stationTemplate
.powerUnit
;
236 delete stationTemplate
.Configuration
;
237 delete stationTemplate
.AutomaticTransactionGenerator
;
238 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
239 delete stationTemplate
.chargePointSerialNumberPrefix
;
240 delete stationTemplate
.meterSerialNumberPrefix
;
241 return stationTemplate
as unknown
as ChargingStationInfo
;
244 public static createStationInfoHash(stationInfo
: ChargingStationInfo
): void {
245 delete stationInfo
.infoHash
;
246 stationInfo
.infoHash
= crypto
247 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
248 .update(JSON
.stringify(stationInfo
))
252 public static createSerialNumber(
253 stationTemplate
: ChargingStationTemplate
,
254 stationInfo
: ChargingStationInfo
,
256 randomSerialNumberUpperCase
?: boolean;
257 randomSerialNumber
?: boolean;
259 randomSerialNumberUpperCase
: true,
260 randomSerialNumber
: true,
263 params
= params
?? {};
264 params
.randomSerialNumberUpperCase
= params
?.randomSerialNumberUpperCase
?? true;
265 params
.randomSerialNumber
= params
?.randomSerialNumber
?? true;
266 const serialNumberSuffix
= params
?.randomSerialNumber
267 ? ChargingStationUtils
.getRandomSerialNumberSuffix({
268 upperCase
: params
.randomSerialNumberUpperCase
,
271 stationInfo
.chargePointSerialNumber
= Utils
.isNotEmptyString(
272 stationTemplate
?.chargePointSerialNumberPrefix
274 ? `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`
276 stationInfo
.chargeBoxSerialNumber
= Utils
.isNotEmptyString(
277 stationTemplate
?.chargeBoxSerialNumberPrefix
279 ? `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`
281 stationInfo
.meterSerialNumber
= Utils
.isNotEmptyString(stationTemplate
?.meterSerialNumberPrefix
)
282 ? `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`
286 public static propagateSerialNumber(
287 stationTemplate
: ChargingStationTemplate
,
288 stationInfoSrc
: ChargingStationInfo
,
289 stationInfoDst
: ChargingStationInfo
291 if (!stationInfoSrc
|| !stationTemplate
) {
293 'Missing charging station template or existing configuration to propagate serial number'
296 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
297 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
298 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
299 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
300 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
301 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
302 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
303 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
304 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
307 public static getAmperageLimitationUnitDivider(stationInfo
: ChargingStationInfo
): number {
309 switch (stationInfo
.amperageLimitationUnit
) {
310 case AmpereUnits
.DECI_AMPERE
:
313 case AmpereUnits
.CENTI_AMPERE
:
316 case AmpereUnits
.MILLI_AMPERE
:
323 public static getChargingStationConnectorChargingProfilesPowerLimit(
324 chargingStation
: ChargingStation
,
326 ): number | undefined {
327 let limit
: number, matchingChargingProfile
: ChargingProfile
;
328 let chargingProfiles
: ChargingProfile
[] = [];
329 // Get charging profiles for connector and sort by stack level
330 chargingProfiles
= chargingStation
331 .getConnectorStatus(connectorId
)
332 ?.chargingProfiles
?.sort((a
, b
) => b
.stackLevel
- a
.stackLevel
);
333 // Get profiles on connector 0
334 if (chargingStation
.getConnectorStatus(0)?.chargingProfiles
) {
335 chargingProfiles
.push(
337 .getConnectorStatus(0)
338 .chargingProfiles
.sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
341 if (Utils
.isNotEmptyArray(chargingProfiles
)) {
342 const result
= ChargingStationUtils
.getLimitFromChargingProfiles(
344 chargingStation
.logPrefix()
346 if (!Utils
.isNullOrUndefined(result
)) {
347 limit
= result
?.limit
;
348 matchingChargingProfile
= result
?.matchingChargingProfile
;
349 switch (chargingStation
.getCurrentOutType()) {
352 matchingChargingProfile
.chargingSchedule
.chargingRateUnit
===
353 ChargingRateUnitType
.WATT
355 : ACElectricUtils
.powerTotal(
356 chargingStation
.getNumberOfPhases(),
357 chargingStation
.getVoltageOut(),
363 matchingChargingProfile
.chargingSchedule
.chargingRateUnit
===
364 ChargingRateUnitType
.WATT
366 : DCElectricUtils
.power(chargingStation
.getVoltageOut(), limit
);
368 const connectorMaximumPower
=
369 chargingStation
.getMaximumPower() / chargingStation
.powerDivider
;
370 if (limit
> connectorMaximumPower
) {
372 `${chargingStation.logPrefix()} Charging profile id ${
373 matchingChargingProfile.chargingProfileId
374 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
377 limit
= connectorMaximumPower
;
384 public static getDefaultVoltageOut(
385 currentType
: CurrentType
,
386 templateFile
: string,
389 const errMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
390 let defaultVoltageOut
: number;
391 switch (currentType
) {
393 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
396 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
399 logger
.error(`${logPrefix} ${errMsg}`);
400 throw new BaseError(errMsg
);
402 return defaultVoltageOut
;
405 public static getAuthorizationFile(stationInfo
: ChargingStationInfo
): string | undefined {
407 stationInfo
.authorizationFile
&&
409 path
.resolve(path
.dirname(fileURLToPath(import.meta
.url
)), '../'),
411 path
.basename(stationInfo
.authorizationFile
)
417 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
419 * @param chargingProfiles -
423 private static getLimitFromChargingProfiles(
424 chargingProfiles
: ChargingProfile
[],
428 matchingChargingProfile
: ChargingProfile
;
430 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
431 for (const chargingProfile
of chargingProfiles
) {
433 const currentMoment
= moment();
434 const chargingSchedule
= chargingProfile
.chargingSchedule
;
435 // Check type (recurring) and if it is already active
436 // Adjust the daily recurring schedule to today
438 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
439 chargingProfile
.recurrencyKind
=== RecurrencyKindType
.DAILY
&&
440 currentMoment
.isAfter(chargingSchedule
.startSchedule
)
442 const currentDate
= new Date();
443 chargingSchedule
.startSchedule
= new Date(chargingSchedule
.startSchedule
);
444 chargingSchedule
.startSchedule
.setFullYear(
445 currentDate
.getFullYear(),
446 currentDate
.getMonth(),
447 currentDate
.getDate()
449 // Check if the start of the schedule is yesterday
450 if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
451 chargingSchedule
.startSchedule
.setDate(currentDate
.getDate() - 1);
453 } else if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
456 // Check if the charging profile is active
458 moment(chargingSchedule
.startSchedule
)
459 .add(chargingSchedule
.duration
, 's')
460 .isAfter(currentMoment
)
462 let lastButOneSchedule
: ChargingSchedulePeriod
;
463 // Search the right schedule period
464 for (const schedulePeriod
of chargingSchedule
.chargingSchedulePeriod
) {
465 // Handling of only one period
467 chargingSchedule
.chargingSchedulePeriod
.length
=== 1 &&
468 schedulePeriod
.startPeriod
=== 0
471 limit
: schedulePeriod
.limit
,
472 matchingChargingProfile
: chargingProfile
,
474 logger
.debug(debugLogMsg
, result
);
477 // Find the right schedule period
479 moment(chargingSchedule
.startSchedule
)
480 .add(schedulePeriod
.startPeriod
, 's')
481 .isAfter(currentMoment
)
483 // Found the schedule: last but one is the correct one
485 limit
: lastButOneSchedule
.limit
,
486 matchingChargingProfile
: chargingProfile
,
488 logger
.debug(debugLogMsg
, result
);
492 lastButOneSchedule
= schedulePeriod
;
493 // Handle the last schedule period
495 schedulePeriod
.startPeriod
===
496 chargingSchedule
.chargingSchedulePeriod
[
497 chargingSchedule
.chargingSchedulePeriod
.length
- 1
501 limit
: lastButOneSchedule
.limit
,
502 matchingChargingProfile
: chargingProfile
,
504 logger
.debug(debugLogMsg
, result
);
513 private static getRandomSerialNumberSuffix(params
?: {
514 randomBytesLength
?: number;
517 const randomSerialNumberSuffix
= crypto
518 .randomBytes(params
?.randomBytesLength
?? 16)
520 if (params
?.upperCase
) {
521 return randomSerialNumberSuffix
.toUpperCase();
523 return randomSerialNumberSuffix
;