1 import crypto from
'crypto';
2 import path from
'path';
3 import { fileURLToPath
} from
'url';
5 import moment from
'moment';
7 import BaseError from
'../exception/BaseError';
8 import type { ChargingStationInfo
} from
'../types/ChargingStationInfo';
11 type ChargingStationTemplate
,
14 } from
'../types/ChargingStationTemplate';
15 import { ChargingProfileKindType
, RecurrencyKindType
} from
'../types/ocpp/1.6/ChargingProfile';
16 import type { ChargingProfile
, ChargingSchedulePeriod
} from
'../types/ocpp/ChargingProfile';
17 import type { BootNotificationRequest
} from
'../types/ocpp/Requests';
18 import { WorkerProcessType
} from
'../types/Worker';
19 import Configuration from
'../utils/Configuration';
20 import Constants from
'../utils/Constants';
21 import logger from
'../utils/Logger';
22 import Utils from
'../utils/Utils';
24 const moduleName
= 'ChargingStationUtils';
26 export class ChargingStationUtils
{
27 private constructor() {
28 // This is intentional
31 public static getChargingStationId(
33 stationTemplate
: ChargingStationTemplate
35 // In case of multiple instances: add instance index to charging station id
36 const instanceIndex
= process
.env
.CF_INSTANCE_INDEX
?? 0;
37 const idSuffix
= stationTemplate
.nameSuffix
?? '';
38 const idStr
= '000000000' + index
.toString();
39 return stationTemplate
?.fixedName
40 ? stationTemplate
.baseName
41 : stationTemplate
.baseName
+
43 instanceIndex
.toString() +
44 idStr
.substring(idStr
.length
- 4) +
48 public static getHashId(index
: number, stationTemplate
: ChargingStationTemplate
): string {
49 const hashBootNotificationRequest
= {
50 chargePointModel
: stationTemplate
.chargePointModel
,
51 chargePointVendor
: stationTemplate
.chargePointVendor
,
52 ...(!Utils
.isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
53 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
55 ...(!Utils
.isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
56 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
58 ...(!Utils
.isUndefined(stationTemplate
.firmwareVersion
) && {
59 firmwareVersion
: stationTemplate
.firmwareVersion
,
61 ...(!Utils
.isUndefined(stationTemplate
.iccid
) && { iccid
: stationTemplate
.iccid
}),
62 ...(!Utils
.isUndefined(stationTemplate
.imsi
) && { imsi
: stationTemplate
.imsi
}),
63 ...(!Utils
.isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
64 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
66 ...(!Utils
.isUndefined(stationTemplate
.meterType
) && {
67 meterType
: stationTemplate
.meterType
,
71 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
73 JSON
.stringify(hashBootNotificationRequest
) +
74 ChargingStationUtils
.getChargingStationId(index
, stationTemplate
)
79 public static getTemplateMaxNumberOfConnectors(stationTemplate
: ChargingStationTemplate
): number {
80 const templateConnectors
= stationTemplate
?.Connectors
;
81 if (!templateConnectors
) {
84 return Object.keys(templateConnectors
).length
;
87 public static checkTemplateMaxConnectors(
88 templateMaxConnectors
: number,
92 if (templateMaxConnectors
=== 0) {
94 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
96 } else if (templateMaxConnectors
< 0) {
98 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
103 public static getConfiguredNumberOfConnectors(stationTemplate
: ChargingStationTemplate
): number {
104 let configuredMaxConnectors
: number;
105 if (Utils
.isEmptyArray(stationTemplate
.numberOfConnectors
) === false) {
106 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
107 configuredMaxConnectors
=
108 numberOfConnectors
[Math.floor(Utils
.secureRandom() * numberOfConnectors
.length
)];
109 } else if (Utils
.isUndefined(stationTemplate
.numberOfConnectors
) === false) {
110 configuredMaxConnectors
= stationTemplate
.numberOfConnectors
as number;
112 configuredMaxConnectors
= stationTemplate
?.Connectors
[0]
113 ? ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
) - 1
114 : ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
);
116 return configuredMaxConnectors
;
119 public static checkConfiguredMaxConnectors(
120 configuredMaxConnectors
: number,
121 templateFile
: string,
124 if (configuredMaxConnectors
<= 0) {
126 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
131 public static createBootNotificationRequest(
132 stationInfo
: ChargingStationInfo
133 ): BootNotificationRequest
{
135 chargePointModel
: stationInfo
.chargePointModel
,
136 chargePointVendor
: stationInfo
.chargePointVendor
,
137 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
138 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
140 ...(!Utils
.isUndefined(stationInfo
.chargePointSerialNumber
) && {
141 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
143 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
144 firmwareVersion
: stationInfo
.firmwareVersion
,
146 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
147 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
148 ...(!Utils
.isUndefined(stationInfo
.meterSerialNumber
) && {
149 meterSerialNumber
: stationInfo
.meterSerialNumber
,
151 ...(!Utils
.isUndefined(stationInfo
.meterType
) && {
152 meterType
: stationInfo
.meterType
,
157 public static workerPoolInUse(): boolean {
158 return [WorkerProcessType
.DYNAMIC_POOL
, WorkerProcessType
.STATIC_POOL
].includes(
159 Configuration
.getWorker().processType
163 public static workerDynamicPoolInUse(): boolean {
164 return Configuration
.getWorker().processType
=== WorkerProcessType
.DYNAMIC_POOL
;
167 public static warnDeprecatedTemplateKey(
168 template
: ChargingStationTemplate
,
170 templateFile
: string,
174 if (!Utils
.isUndefined(template
[key
])) {
176 `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
177 logMsgToAppend && '. ' + logMsgToAppend
183 public static convertDeprecatedTemplateKey(
184 template
: ChargingStationTemplate
,
185 deprecatedKey
: string,
188 if (!Utils
.isUndefined(template
[deprecatedKey
])) {
189 template
[key
] = template
[deprecatedKey
] as unknown
;
190 delete template
[deprecatedKey
];
194 public static stationTemplateToStationInfo(
195 stationTemplate
: ChargingStationTemplate
196 ): ChargingStationInfo
{
197 stationTemplate
= Utils
.cloneObject(stationTemplate
);
198 delete stationTemplate
.power
;
199 delete stationTemplate
.powerUnit
;
200 delete stationTemplate
.Configuration
;
201 delete stationTemplate
.AutomaticTransactionGenerator
;
202 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
203 delete stationTemplate
.chargePointSerialNumberPrefix
;
204 delete stationTemplate
.meterSerialNumberPrefix
;
205 return stationTemplate
as unknown
as ChargingStationInfo
;
208 public static createStationInfoHash(stationInfo
: ChargingStationInfo
): void {
209 delete stationInfo
.infoHash
;
210 stationInfo
.infoHash
= crypto
211 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
212 .update(JSON
.stringify(stationInfo
))
216 public static createSerialNumber(
217 stationTemplate
: ChargingStationTemplate
,
218 stationInfo
: ChargingStationInfo
= {} as ChargingStationInfo
,
220 randomSerialNumberUpperCase
?: boolean;
221 randomSerialNumber
?: boolean;
223 randomSerialNumberUpperCase
: true,
224 randomSerialNumber
: true,
227 params
= params
?? {};
228 params
.randomSerialNumberUpperCase
= params
?.randomSerialNumberUpperCase
?? true;
229 params
.randomSerialNumber
= params
?.randomSerialNumber
?? true;
230 const serialNumberSuffix
= params
?.randomSerialNumber
231 ? ChargingStationUtils
.getRandomSerialNumberSuffix({
232 upperCase
: params
.randomSerialNumberUpperCase
,
235 stationInfo
.chargePointSerialNumber
=
236 stationTemplate
?.chargePointSerialNumberPrefix
&&
237 stationTemplate
.chargePointSerialNumberPrefix
+ serialNumberSuffix
;
238 stationInfo
.chargeBoxSerialNumber
=
239 stationTemplate
?.chargeBoxSerialNumberPrefix
&&
240 stationTemplate
.chargeBoxSerialNumberPrefix
+ serialNumberSuffix
;
241 stationInfo
.meterSerialNumber
=
242 stationTemplate
?.meterSerialNumberPrefix
&&
243 stationTemplate
.meterSerialNumberPrefix
+ serialNumberSuffix
;
246 public static propagateSerialNumber(
247 stationTemplate
: ChargingStationTemplate
,
248 stationInfoSrc
: ChargingStationInfo
,
249 stationInfoDst
: ChargingStationInfo
= {} as ChargingStationInfo
251 if (!stationInfoSrc
|| !stationTemplate
) {
253 'Missing charging station template or existing configuration to propagate serial number'
256 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
257 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
258 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
259 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
260 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
261 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
262 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
263 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
264 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
267 public static getAmperageLimitationUnitDivider(stationInfo
: ChargingStationInfo
): number {
269 switch (stationInfo
.amperageLimitationUnit
) {
270 case AmpereUnits
.DECI_AMPERE
:
273 case AmpereUnits
.CENTI_AMPERE
:
276 case AmpereUnits
.MILLI_AMPERE
:
284 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
286 * @param {ChargingProfile[]} chargingProfiles
287 * @param {string} logPrefix
288 * @returns {{ limit, matchingChargingProfile }}
290 public static getLimitFromChargingProfiles(
291 chargingProfiles
: ChargingProfile
[],
295 matchingChargingProfile
: ChargingProfile
;
297 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
298 for (const chargingProfile
of chargingProfiles
) {
300 const currentMoment
= moment();
301 const chargingSchedule
= chargingProfile
.chargingSchedule
;
302 // Check type (recurring) and if it is already active
303 // Adjust the daily recurring schedule to today
305 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
306 chargingProfile
.recurrencyKind
=== RecurrencyKindType
.DAILY
&&
307 currentMoment
.isAfter(chargingSchedule
.startSchedule
)
309 const currentDate
= new Date();
310 chargingSchedule
.startSchedule
= new Date(chargingSchedule
.startSchedule
);
311 chargingSchedule
.startSchedule
.setFullYear(
312 currentDate
.getFullYear(),
313 currentDate
.getMonth(),
314 currentDate
.getDate()
316 // Check if the start of the schedule is yesterday
317 if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
318 chargingSchedule
.startSchedule
.setDate(currentDate
.getDate() - 1);
320 } else if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
323 // Check if the charging profile is active
325 moment(chargingSchedule
.startSchedule
)
326 .add(chargingSchedule
.duration
, 's')
327 .isAfter(currentMoment
)
329 let lastButOneSchedule
: ChargingSchedulePeriod
;
330 // Search the right schedule period
331 for (const schedulePeriod
of chargingSchedule
.chargingSchedulePeriod
) {
332 // Handling of only one period
334 chargingSchedule
.chargingSchedulePeriod
.length
=== 1 &&
335 schedulePeriod
.startPeriod
=== 0
338 limit
: schedulePeriod
.limit
,
339 matchingChargingProfile
: chargingProfile
,
341 logger
.debug(debugLogMsg
, result
);
344 // Find the right schedule period
346 moment(chargingSchedule
.startSchedule
)
347 .add(schedulePeriod
.startPeriod
, 's')
348 .isAfter(currentMoment
)
350 // Found the schedule: last but one is the correct one
352 limit
: lastButOneSchedule
.limit
,
353 matchingChargingProfile
: chargingProfile
,
355 logger
.debug(debugLogMsg
, result
);
359 lastButOneSchedule
= schedulePeriod
;
360 // Handle the last schedule period
362 schedulePeriod
.startPeriod
===
363 chargingSchedule
.chargingSchedulePeriod
[
364 chargingSchedule
.chargingSchedulePeriod
.length
- 1
368 limit
: lastButOneSchedule
.limit
,
369 matchingChargingProfile
: chargingProfile
,
371 logger
.debug(debugLogMsg
, result
);
380 public static getDefaultVoltageOut(
381 currentType
: CurrentType
,
382 templateFile
: string,
385 const errMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
386 let defaultVoltageOut
: number;
387 switch (currentType
) {
389 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
392 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
395 logger
.error(`${logPrefix} ${errMsg}`);
396 throw new BaseError(errMsg
);
398 return defaultVoltageOut
;
401 public static getAuthorizationFile(stationInfo
: ChargingStationInfo
): string | undefined {
403 stationInfo
.authorizationFile
&&
405 path
.resolve(path
.dirname(fileURLToPath(import.meta
.url
)), '../'),
407 path
.basename(stationInfo
.authorizationFile
)
412 private static getRandomSerialNumberSuffix(params
?: {
413 randomBytesLength
?: number;
416 const randomSerialNumberSuffix
= crypto
417 .randomBytes(params
?.randomBytesLength
?? 16)
419 if (params
?.upperCase
) {
420 return randomSerialNumberSuffix
.toUpperCase();
422 return randomSerialNumberSuffix
;