1 import crypto from
'node:crypto';
2 import path from
'path';
3 import { fileURLToPath
} from
'url';
5 import moment from
'moment';
7 import type ChargingStation from
'./ChargingStation';
8 import BaseError from
'../exception/BaseError';
9 import type { ChargingStationInfo
} from
'../types/ChargingStationInfo';
12 type ChargingStationTemplate
,
15 } from
'../types/ChargingStationTemplate';
16 import { ChargingProfileKindType
, RecurrencyKindType
} from
'../types/ocpp/1.6/ChargingProfile';
17 import type { OCPP16BootNotificationRequest
} from
'../types/ocpp/1.6/Requests';
18 import { BootReasonEnumType
, type OCPP20BootNotificationRequest
} from
'../types/ocpp/2.0/Requests';
22 type ChargingSchedulePeriod
,
23 } from
'../types/ocpp/ChargingProfile';
24 import { OCPPVersion
} from
'../types/ocpp/OCPPVersion';
25 import type { BootNotificationRequest
} from
'../types/ocpp/Requests';
26 import { WorkerProcessType
} from
'../types/Worker';
27 import Configuration from
'../utils/Configuration';
28 import Constants from
'../utils/Constants';
29 import { ACElectricUtils
, DCElectricUtils
} from
'../utils/ElectricUtils';
30 import logger from
'../utils/Logger';
31 import Utils from
'../utils/Utils';
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
) +
82 ChargingStationUtils
.getChargingStationId(index
, stationTemplate
)
87 public static getTemplateMaxNumberOfConnectors(stationTemplate
: ChargingStationTemplate
): number {
88 const templateConnectors
= stationTemplate
?.Connectors
;
89 if (!templateConnectors
) {
92 return Object.keys(templateConnectors
).length
;
95 public static checkTemplateMaxConnectors(
96 templateMaxConnectors
: number,
100 if (templateMaxConnectors
=== 0) {
102 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
104 } else if (templateMaxConnectors
< 0) {
106 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
111 public static getConfiguredNumberOfConnectors(stationTemplate
: ChargingStationTemplate
): number {
112 let configuredMaxConnectors
: number;
113 if (Utils
.isEmptyArray(stationTemplate
.numberOfConnectors
) === false) {
114 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
115 configuredMaxConnectors
=
116 numberOfConnectors
[Math.floor(Utils
.secureRandom() * numberOfConnectors
.length
)];
117 } else if (Utils
.isUndefined(stationTemplate
.numberOfConnectors
) === false) {
118 configuredMaxConnectors
= stationTemplate
.numberOfConnectors
as number;
120 configuredMaxConnectors
= stationTemplate
?.Connectors
[0]
121 ? ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
) - 1
122 : ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
);
124 return configuredMaxConnectors
;
127 public static checkConfiguredMaxConnectors(
128 configuredMaxConnectors
: number,
129 templateFile
: string,
132 if (configuredMaxConnectors
<= 0) {
134 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
139 public static createBootNotificationRequest(
140 stationInfo
: ChargingStationInfo
,
141 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
142 ): BootNotificationRequest
{
143 const ocppVersion
= stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
;
144 switch (ocppVersion
) {
145 case OCPPVersion
.VERSION_16
:
147 chargePointModel
: stationInfo
.chargePointModel
,
148 chargePointVendor
: stationInfo
.chargePointVendor
,
149 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
150 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
152 ...(!Utils
.isUndefined(stationInfo
.chargePointSerialNumber
) && {
153 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
155 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
156 firmwareVersion
: stationInfo
.firmwareVersion
,
158 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
159 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
160 ...(!Utils
.isUndefined(stationInfo
.meterSerialNumber
) && {
161 meterSerialNumber
: stationInfo
.meterSerialNumber
,
163 ...(!Utils
.isUndefined(stationInfo
.meterType
) && {
164 meterType
: stationInfo
.meterType
,
166 } as OCPP16BootNotificationRequest
;
167 case OCPPVersion
.VERSION_20
:
168 case OCPPVersion
.VERSION_201
:
172 model
: stationInfo
.chargePointModel
,
173 vendorName
: stationInfo
.chargePointVendor
,
174 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
175 firmwareVersion
: stationInfo
.firmwareVersion
,
177 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
178 serialNumber
: stationInfo
.chargeBoxSerialNumber
,
180 ...((!Utils
.isUndefined(stationInfo
.iccid
) || !Utils
.isUndefined(stationInfo
.imsi
)) && {
182 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
183 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
187 } as OCPP20BootNotificationRequest
;
191 public static workerPoolInUse(): boolean {
192 return [WorkerProcessType
.DYNAMIC_POOL
, WorkerProcessType
.STATIC_POOL
].includes(
193 Configuration
.getWorker().processType
197 public static workerDynamicPoolInUse(): boolean {
198 return Configuration
.getWorker().processType
=== WorkerProcessType
.DYNAMIC_POOL
;
201 public static warnDeprecatedTemplateKey(
202 template
: ChargingStationTemplate
,
204 templateFile
: string,
208 if (!Utils
.isUndefined(template
[key
])) {
210 `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
211 logMsgToAppend && `. ${logMsgToAppend}
`
217 public static convertDeprecatedTemplateKey(
218 template
: ChargingStationTemplate
,
219 deprecatedKey
: string,
222 if (!Utils
.isUndefined(template
[deprecatedKey
])) {
223 template
[key
] = template
[deprecatedKey
] as unknown
;
224 delete template
[deprecatedKey
];
228 public static stationTemplateToStationInfo(
229 stationTemplate
: ChargingStationTemplate
230 ): ChargingStationInfo
{
231 stationTemplate
= Utils
.cloneObject(stationTemplate
);
232 delete stationTemplate
.power
;
233 delete stationTemplate
.powerUnit
;
234 delete stationTemplate
.Configuration
;
235 delete stationTemplate
.AutomaticTransactionGenerator
;
236 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
237 delete stationTemplate
.chargePointSerialNumberPrefix
;
238 delete stationTemplate
.meterSerialNumberPrefix
;
239 return stationTemplate
as unknown
as ChargingStationInfo
;
242 public static createStationInfoHash(stationInfo
: ChargingStationInfo
): void {
243 delete stationInfo
.infoHash
;
244 stationInfo
.infoHash
= crypto
245 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
246 .update(JSON
.stringify(stationInfo
))
250 public static createSerialNumber(
251 stationTemplate
: ChargingStationTemplate
,
252 stationInfo
: ChargingStationInfo
,
254 randomSerialNumberUpperCase
?: boolean;
255 randomSerialNumber
?: boolean;
257 randomSerialNumberUpperCase
: true,
258 randomSerialNumber
: true,
261 params
= params
?? {};
262 params
.randomSerialNumberUpperCase
= params
?.randomSerialNumberUpperCase
?? true;
263 params
.randomSerialNumber
= params
?.randomSerialNumber
?? true;
264 const serialNumberSuffix
= params
?.randomSerialNumber
265 ? ChargingStationUtils
.getRandomSerialNumberSuffix({
266 upperCase
: params
.randomSerialNumberUpperCase
,
269 stationInfo
.chargePointSerialNumber
=
270 stationTemplate
?.chargePointSerialNumberPrefix
&&
271 stationTemplate
.chargePointSerialNumberPrefix
+ serialNumberSuffix
;
272 stationInfo
.chargeBoxSerialNumber
=
273 stationTemplate
?.chargeBoxSerialNumberPrefix
&&
274 stationTemplate
.chargeBoxSerialNumberPrefix
+ serialNumberSuffix
;
275 stationInfo
.meterSerialNumber
=
276 stationTemplate
?.meterSerialNumberPrefix
&&
277 stationTemplate
.meterSerialNumberPrefix
+ serialNumberSuffix
;
280 public static propagateSerialNumber(
281 stationTemplate
: ChargingStationTemplate
,
282 stationInfoSrc
: ChargingStationInfo
,
283 stationInfoDst
: ChargingStationInfo
285 if (!stationInfoSrc
|| !stationTemplate
) {
287 'Missing charging station template or existing configuration to propagate serial number'
290 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
291 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
292 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
293 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
294 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
295 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
296 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
297 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
298 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
301 public static getAmperageLimitationUnitDivider(stationInfo
: ChargingStationInfo
): number {
303 switch (stationInfo
.amperageLimitationUnit
) {
304 case AmpereUnits
.DECI_AMPERE
:
307 case AmpereUnits
.CENTI_AMPERE
:
310 case AmpereUnits
.MILLI_AMPERE
:
317 public static getChargingStationConnectorChargingProfilesPowerLimit(
318 chargingStation
: ChargingStation
,
320 ): number | undefined {
321 let limit
: number, matchingChargingProfile
: ChargingProfile
;
322 let chargingProfiles
: ChargingProfile
[] = [];
323 // Get charging profiles for connector and sort by stack level
324 chargingProfiles
= chargingStation
325 .getConnectorStatus(connectorId
)
326 .chargingProfiles
.sort((a
, b
) => b
.stackLevel
- a
.stackLevel
);
327 // Get profiles on connector 0
328 if (chargingStation
.getConnectorStatus(0).chargingProfiles
) {
329 chargingProfiles
.push(
331 .getConnectorStatus(0)
332 .chargingProfiles
.sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
335 if (!Utils
.isEmptyArray(chargingProfiles
)) {
336 const result
= ChargingStationUtils
.getLimitFromChargingProfiles(
338 chargingStation
.logPrefix()
340 if (!Utils
.isNullOrUndefined(result
)) {
341 limit
= result
.limit
;
342 matchingChargingProfile
= result
.matchingChargingProfile
;
343 switch (chargingStation
.getCurrentOutType()) {
346 matchingChargingProfile
.chargingSchedule
.chargingRateUnit
===
347 ChargingRateUnitType
.WATT
349 : ACElectricUtils
.powerTotal(
350 chargingStation
.getNumberOfPhases(),
351 chargingStation
.getVoltageOut(),
357 matchingChargingProfile
.chargingSchedule
.chargingRateUnit
===
358 ChargingRateUnitType
.WATT
360 : DCElectricUtils
.power(chargingStation
.getVoltageOut(), limit
);
362 const connectorMaximumPower
=
363 chargingStation
.getMaximumPower() / chargingStation
.powerDivider
;
364 if (limit
> connectorMaximumPower
) {
366 `${chargingStation.logPrefix()} Charging profile id ${
367 matchingChargingProfile.chargingProfileId
368 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
371 limit
= connectorMaximumPower
;
378 public static getDefaultVoltageOut(
379 currentType
: CurrentType
,
380 templateFile
: string,
383 const errMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
384 let defaultVoltageOut
: number;
385 switch (currentType
) {
387 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
390 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
393 logger
.error(`${logPrefix} ${errMsg}`);
394 throw new BaseError(errMsg
);
396 return defaultVoltageOut
;
399 public static getAuthorizationFile(stationInfo
: ChargingStationInfo
): string | undefined {
401 stationInfo
.authorizationFile
&&
403 path
.resolve(path
.dirname(fileURLToPath(import.meta
.url
)), '../'),
405 path
.basename(stationInfo
.authorizationFile
)
411 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
413 * @param chargingProfiles -
417 private static getLimitFromChargingProfiles(
418 chargingProfiles
: ChargingProfile
[],
422 matchingChargingProfile
: ChargingProfile
;
424 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
425 for (const chargingProfile
of chargingProfiles
) {
427 const currentMoment
= moment();
428 const chargingSchedule
= chargingProfile
.chargingSchedule
;
429 // Check type (recurring) and if it is already active
430 // Adjust the daily recurring schedule to today
432 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
433 chargingProfile
.recurrencyKind
=== RecurrencyKindType
.DAILY
&&
434 currentMoment
.isAfter(chargingSchedule
.startSchedule
)
436 const currentDate
= new Date();
437 chargingSchedule
.startSchedule
= new Date(chargingSchedule
.startSchedule
);
438 chargingSchedule
.startSchedule
.setFullYear(
439 currentDate
.getFullYear(),
440 currentDate
.getMonth(),
441 currentDate
.getDate()
443 // Check if the start of the schedule is yesterday
444 if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
445 chargingSchedule
.startSchedule
.setDate(currentDate
.getDate() - 1);
447 } else if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
450 // Check if the charging profile is active
452 moment(chargingSchedule
.startSchedule
)
453 .add(chargingSchedule
.duration
, 's')
454 .isAfter(currentMoment
)
456 let lastButOneSchedule
: ChargingSchedulePeriod
;
457 // Search the right schedule period
458 for (const schedulePeriod
of chargingSchedule
.chargingSchedulePeriod
) {
459 // Handling of only one period
461 chargingSchedule
.chargingSchedulePeriod
.length
=== 1 &&
462 schedulePeriod
.startPeriod
=== 0
465 limit
: schedulePeriod
.limit
,
466 matchingChargingProfile
: chargingProfile
,
468 logger
.debug(debugLogMsg
, result
);
471 // Find the right schedule period
473 moment(chargingSchedule
.startSchedule
)
474 .add(schedulePeriod
.startPeriod
, 's')
475 .isAfter(currentMoment
)
477 // Found the schedule: last but one is the correct one
479 limit
: lastButOneSchedule
.limit
,
480 matchingChargingProfile
: chargingProfile
,
482 logger
.debug(debugLogMsg
, result
);
486 lastButOneSchedule
= schedulePeriod
;
487 // Handle the last schedule period
489 schedulePeriod
.startPeriod
===
490 chargingSchedule
.chargingSchedulePeriod
[
491 chargingSchedule
.chargingSchedulePeriod
.length
- 1
495 limit
: lastButOneSchedule
.limit
,
496 matchingChargingProfile
: chargingProfile
,
498 logger
.debug(debugLogMsg
, result
);
507 private static getRandomSerialNumberSuffix(params
?: {
508 randomBytesLength
?: number;
511 const randomSerialNumberSuffix
= crypto
512 .randomBytes(params
?.randomBytesLength
?? 16)
514 if (params
?.upperCase
) {
515 return randomSerialNumberSuffix
.toUpperCase();
517 return randomSerialNumberSuffix
;