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 type { SampledValueTemplate
} from
'../types/MeasurandPerPhaseSampledValueTemplates';
16 import { ChargingProfileKindType
, RecurrencyKindType
} from
'../types/ocpp/1.6/ChargingProfile';
17 import type { ChargingProfile
, ChargingSchedulePeriod
} from
'../types/ocpp/ChargingProfile';
18 import { StandardParametersKey
} from
'../types/ocpp/Configuration';
19 import { MeterValueMeasurand
, MeterValuePhase
} from
'../types/ocpp/MeterValues';
21 type BootNotificationRequest
,
22 IncomingRequestCommand
,
24 } from
'../types/ocpp/Requests';
25 import { WorkerProcessType
} from
'../types/Worker';
26 import Configuration from
'../utils/Configuration';
27 import Constants from
'../utils/Constants';
28 import logger from
'../utils/Logger';
29 import Utils from
'../utils/Utils';
30 import type ChargingStation from
'./ChargingStation';
31 import { ChargingStationConfigurationUtils
} from
'./ChargingStationConfigurationUtils';
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
+
52 instanceIndex
.toString() +
53 idStr
.substring(idStr
.length
- 4) +
57 public static getHashId(index
: number, stationTemplate
: ChargingStationTemplate
): string {
58 const hashBootNotificationRequest
= {
59 chargePointModel
: stationTemplate
.chargePointModel
,
60 chargePointVendor
: stationTemplate
.chargePointVendor
,
61 ...(!Utils
.isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
62 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
64 ...(!Utils
.isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
65 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
67 ...(!Utils
.isUndefined(stationTemplate
.firmwareVersion
) && {
68 firmwareVersion
: stationTemplate
.firmwareVersion
,
70 ...(!Utils
.isUndefined(stationTemplate
.iccid
) && { iccid
: stationTemplate
.iccid
}),
71 ...(!Utils
.isUndefined(stationTemplate
.imsi
) && { imsi
: stationTemplate
.imsi
}),
72 ...(!Utils
.isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
73 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
75 ...(!Utils
.isUndefined(stationTemplate
.meterType
) && {
76 meterType
: stationTemplate
.meterType
,
80 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
82 JSON
.stringify(hashBootNotificationRequest
) +
83 ChargingStationUtils
.getChargingStationId(index
, stationTemplate
)
88 public static getTemplateMaxNumberOfConnectors(stationTemplate
: ChargingStationTemplate
): number {
89 const templateConnectors
= stationTemplate
?.Connectors
;
90 if (!templateConnectors
) {
93 return Object.keys(templateConnectors
).length
;
96 public static checkTemplateMaxConnectors(
97 templateMaxConnectors
: number,
101 if (templateMaxConnectors
=== 0) {
103 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
105 } else if (templateMaxConnectors
< 0) {
107 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
112 public static getConfiguredNumberOfConnectors(stationTemplate
: ChargingStationTemplate
): number {
113 let configuredMaxConnectors
: number;
114 if (Utils
.isEmptyArray(stationTemplate
.numberOfConnectors
) === false) {
115 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
116 configuredMaxConnectors
=
117 numberOfConnectors
[Math.floor(Utils
.secureRandom() * numberOfConnectors
.length
)];
118 } else if (Utils
.isUndefined(stationTemplate
.numberOfConnectors
) === false) {
119 configuredMaxConnectors
= stationTemplate
.numberOfConnectors
as number;
121 configuredMaxConnectors
= stationTemplate
?.Connectors
[0]
122 ? ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
) - 1
123 : ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
);
125 return configuredMaxConnectors
;
128 public static checkConfiguredMaxConnectors(
129 configuredMaxConnectors
: number,
130 templateFile
: string,
133 if (configuredMaxConnectors
<= 0) {
135 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
140 public static createBootNotificationRequest(
141 stationInfo
: ChargingStationInfo
142 ): BootNotificationRequest
{
144 chargePointModel
: stationInfo
.chargePointModel
,
145 chargePointVendor
: stationInfo
.chargePointVendor
,
146 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
147 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
149 ...(!Utils
.isUndefined(stationInfo
.chargePointSerialNumber
) && {
150 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
152 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
153 firmwareVersion
: stationInfo
.firmwareVersion
,
155 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
156 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
157 ...(!Utils
.isUndefined(stationInfo
.meterSerialNumber
) && {
158 meterSerialNumber
: stationInfo
.meterSerialNumber
,
160 ...(!Utils
.isUndefined(stationInfo
.meterType
) && {
161 meterType
: stationInfo
.meterType
,
166 public static workerPoolInUse(): boolean {
167 return [WorkerProcessType
.DYNAMIC_POOL
, WorkerProcessType
.STATIC_POOL
].includes(
168 Configuration
.getWorker().processType
172 public static workerDynamicPoolInUse(): boolean {
173 return Configuration
.getWorker().processType
=== WorkerProcessType
.DYNAMIC_POOL
;
176 public static warnDeprecatedTemplateKey(
177 template
: ChargingStationTemplate
,
179 templateFile
: string,
183 if (!Utils
.isUndefined(template
[key
])) {
185 `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
186 logMsgToAppend && '. ' + logMsgToAppend
192 public static convertDeprecatedTemplateKey(
193 template
: ChargingStationTemplate
,
194 deprecatedKey
: string,
197 if (!Utils
.isUndefined(template
[deprecatedKey
])) {
198 template
[key
] = template
[deprecatedKey
] as unknown
;
199 delete template
[deprecatedKey
];
203 public static stationTemplateToStationInfo(
204 stationTemplate
: ChargingStationTemplate
205 ): ChargingStationInfo
{
206 stationTemplate
= Utils
.cloneObject(stationTemplate
);
207 delete stationTemplate
.power
;
208 delete stationTemplate
.powerUnit
;
209 delete stationTemplate
.Configuration
;
210 delete stationTemplate
.AutomaticTransactionGenerator
;
211 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
212 delete stationTemplate
.chargePointSerialNumberPrefix
;
213 delete stationTemplate
.meterSerialNumberPrefix
;
214 return stationTemplate
as unknown
as ChargingStationInfo
;
217 public static createStationInfoHash(stationInfo
: ChargingStationInfo
): void {
218 delete stationInfo
.infoHash
;
219 stationInfo
.infoHash
= crypto
220 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
221 .update(JSON
.stringify(stationInfo
))
225 public static createSerialNumber(
226 stationTemplate
: ChargingStationTemplate
,
227 stationInfo
: ChargingStationInfo
= {} as ChargingStationInfo
,
229 randomSerialNumberUpperCase
?: boolean;
230 randomSerialNumber
?: boolean;
232 randomSerialNumberUpperCase
: true,
233 randomSerialNumber
: true,
236 params
= params
?? {};
237 params
.randomSerialNumberUpperCase
= params
?.randomSerialNumberUpperCase
?? true;
238 params
.randomSerialNumber
= params
?.randomSerialNumber
?? true;
239 const serialNumberSuffix
= params
?.randomSerialNumber
240 ? ChargingStationUtils
.getRandomSerialNumberSuffix({
241 upperCase
: params
.randomSerialNumberUpperCase
,
244 stationInfo
.chargePointSerialNumber
=
245 stationTemplate
?.chargePointSerialNumberPrefix
&&
246 stationTemplate
.chargePointSerialNumberPrefix
+ serialNumberSuffix
;
247 stationInfo
.chargeBoxSerialNumber
=
248 stationTemplate
?.chargeBoxSerialNumberPrefix
&&
249 stationTemplate
.chargeBoxSerialNumberPrefix
+ serialNumberSuffix
;
250 stationInfo
.meterSerialNumber
=
251 stationTemplate
?.meterSerialNumberPrefix
&&
252 stationTemplate
.meterSerialNumberPrefix
+ serialNumberSuffix
;
255 public static propagateSerialNumber(
256 stationTemplate
: ChargingStationTemplate
,
257 stationInfoSrc
: ChargingStationInfo
,
258 stationInfoDst
: ChargingStationInfo
= {} as ChargingStationInfo
260 if (!stationInfoSrc
|| !stationTemplate
) {
262 'Missing charging station template or existing configuration to propagate serial number'
265 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
266 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
267 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
268 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
269 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
270 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
271 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
272 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
273 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
276 public static getAmperageLimitationUnitDivider(stationInfo
: ChargingStationInfo
): number {
278 switch (stationInfo
.amperageLimitationUnit
) {
279 case AmpereUnits
.DECI_AMPERE
:
282 case AmpereUnits
.CENTI_AMPERE
:
285 case AmpereUnits
.MILLI_AMPERE
:
292 public static setChargingProfile(
293 chargingStation
: ChargingStation
,
297 if (Utils
.isNullOrUndefined(chargingStation
.getConnectorStatus(connectorId
).chargingProfiles
)) {
299 `${chargingStation.logPrefix()} Trying to set a charging profile on connectorId ${connectorId} with an uninitialized charging profiles array attribute, applying deferred initialization`
301 chargingStation
.getConnectorStatus(connectorId
).chargingProfiles
= [];
303 if (Array.isArray(chargingStation
.getConnectorStatus(connectorId
).chargingProfiles
) === false) {
305 `${chargingStation.logPrefix()} Trying to set a charging profile on connectorId ${connectorId} with an improper attribute type for the charging profiles array, applying proper type initialization`
307 chargingStation
.getConnectorStatus(connectorId
).chargingProfiles
= [];
309 let cpReplaced
= false;
310 if (!Utils
.isEmptyArray(chargingStation
.getConnectorStatus(connectorId
).chargingProfiles
)) {
312 .getConnectorStatus(connectorId
)
313 .chargingProfiles
?.forEach((chargingProfile
: ChargingProfile
, index
: number) => {
315 chargingProfile
.chargingProfileId
=== cp
.chargingProfileId
||
316 (chargingProfile
.stackLevel
=== cp
.stackLevel
&&
317 chargingProfile
.chargingProfilePurpose
=== cp
.chargingProfilePurpose
)
319 chargingStation
.getConnectorStatus(connectorId
).chargingProfiles
[index
] = cp
;
324 !cpReplaced
&& chargingStation
.getConnectorStatus(connectorId
).chargingProfiles
?.push(cp
);
328 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
330 * @param {ChargingProfile[]} chargingProfiles
331 * @param {string} logPrefix
332 * @returns {{ limit, matchingChargingProfile }}
334 public static getLimitFromChargingProfiles(
335 chargingProfiles
: ChargingProfile
[],
339 matchingChargingProfile
: ChargingProfile
;
341 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
342 for (const chargingProfile
of chargingProfiles
) {
344 const currentMoment
= moment();
345 const chargingSchedule
= chargingProfile
.chargingSchedule
;
346 // Check type (recurring) and if it is already active
347 // Adjust the daily recurring schedule to today
349 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
350 chargingProfile
.recurrencyKind
=== RecurrencyKindType
.DAILY
&&
351 currentMoment
.isAfter(chargingSchedule
.startSchedule
)
353 const currentDate
= new Date();
354 chargingSchedule
.startSchedule
= new Date(chargingSchedule
.startSchedule
);
355 chargingSchedule
.startSchedule
.setFullYear(
356 currentDate
.getFullYear(),
357 currentDate
.getMonth(),
358 currentDate
.getDate()
360 // Check if the start of the schedule is yesterday
361 if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
362 chargingSchedule
.startSchedule
.setDate(currentDate
.getDate() - 1);
364 } else if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
367 // Check if the charging profile is active
369 moment(chargingSchedule
.startSchedule
)
370 .add(chargingSchedule
.duration
, 's')
371 .isAfter(currentMoment
)
373 let lastButOneSchedule
: ChargingSchedulePeriod
;
374 // Search the right schedule period
375 for (const schedulePeriod
of chargingSchedule
.chargingSchedulePeriod
) {
376 // Handling of only one period
378 chargingSchedule
.chargingSchedulePeriod
.length
=== 1 &&
379 schedulePeriod
.startPeriod
=== 0
382 limit
: schedulePeriod
.limit
,
383 matchingChargingProfile
: chargingProfile
,
385 logger
.debug(debugLogMsg
, result
);
388 // Find the right schedule period
390 moment(chargingSchedule
.startSchedule
)
391 .add(schedulePeriod
.startPeriod
, 's')
392 .isAfter(currentMoment
)
394 // Found the schedule: last but one is the correct one
396 limit
: lastButOneSchedule
.limit
,
397 matchingChargingProfile
: chargingProfile
,
399 logger
.debug(debugLogMsg
, result
);
403 lastButOneSchedule
= schedulePeriod
;
404 // Handle the last schedule period
406 schedulePeriod
.startPeriod
===
407 chargingSchedule
.chargingSchedulePeriod
[
408 chargingSchedule
.chargingSchedulePeriod
.length
- 1
412 limit
: lastButOneSchedule
.limit
,
413 matchingChargingProfile
: chargingProfile
,
415 logger
.debug(debugLogMsg
, result
);
424 public static getDefaultVoltageOut(
425 currentType
: CurrentType
,
426 templateFile
: string,
429 const errMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
430 let defaultVoltageOut
: number;
431 switch (currentType
) {
433 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
436 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
439 logger
.error(`${logPrefix} ${errMsg}`);
440 throw new BaseError(errMsg
);
442 return defaultVoltageOut
;
445 public static getSampledValueTemplate(
446 chargingStation
: ChargingStation
,
448 measurand
: MeterValueMeasurand
= MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
449 phase
?: MeterValuePhase
450 ): SampledValueTemplate
| undefined {
451 const onPhaseStr
= phase
? `on phase ${phase} ` : '';
452 if (Constants
.SUPPORTED_MEASURANDS
.includes(measurand
) === false) {
454 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
459 measurand
!== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
460 !ChargingStationConfigurationUtils
.getConfigurationKey(
462 StandardParametersKey
.MeterValuesSampledData
463 )?.value
.includes(measurand
)
466 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId} not found in '${
467 StandardParametersKey.MeterValuesSampledData
472 const sampledValueTemplates
: SampledValueTemplate
[] =
473 chargingStation
.getConnectorStatus(connectorId
).MeterValues
;
476 !Utils
.isEmptyArray(sampledValueTemplates
) && index
< sampledValueTemplates
.length
;
480 Constants
.SUPPORTED_MEASURANDS
.includes(
481 sampledValueTemplates
[index
]?.measurand
??
482 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
486 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
490 sampledValueTemplates
[index
]?.phase
=== phase
&&
491 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
492 ChargingStationConfigurationUtils
.getConfigurationKey(
494 StandardParametersKey
.MeterValuesSampledData
495 )?.value
.includes(measurand
) === true
497 return sampledValueTemplates
[index
];
500 !sampledValueTemplates
[index
].phase
&&
501 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
502 ChargingStationConfigurationUtils
.getConfigurationKey(
504 StandardParametersKey
.MeterValuesSampledData
505 )?.value
.includes(measurand
) === true
507 return sampledValueTemplates
[index
];
509 measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
510 (!sampledValueTemplates
[index
].measurand
||
511 sampledValueTemplates
[index
].measurand
=== measurand
)
513 return sampledValueTemplates
[index
];
516 if (measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
) {
517 const errorMsg
= `Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`;
518 logger
.error(`${chargingStation.logPrefix()} ${errorMsg}`);
519 throw new BaseError(errorMsg
);
522 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
526 public static getAuthorizationFile(stationInfo
: ChargingStationInfo
): string | undefined {
528 stationInfo
.authorizationFile
&&
530 path
.resolve(path
.dirname(fileURLToPath(import.meta
.url
)), '../'),
532 path
.basename(stationInfo
.authorizationFile
)
537 public static isRequestCommandSupported(
538 command
: RequestCommand
,
539 chargingStation
: ChargingStation
541 const isRequestCommand
= Object.values(RequestCommand
).includes(command
);
543 isRequestCommand
=== true &&
544 !chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
548 isRequestCommand
=== true &&
549 chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
551 return chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
[command
] ?? false;
553 logger
.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
557 public static isIncomingRequestCommandSupported(
558 command
: IncomingRequestCommand
,
559 chargingStation
: ChargingStation
561 const isIncomingRequestCommand
= Object.values(IncomingRequestCommand
).includes(command
);
563 isIncomingRequestCommand
=== true &&
564 !chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
568 isIncomingRequestCommand
=== true &&
569 chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
571 return chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
[command
] ?? false;
573 logger
.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
577 private static getRandomSerialNumberSuffix(params
?: {
578 randomBytesLength
?: number;
581 const randomSerialNumberSuffix
= crypto
582 .randomBytes(params
?.randomBytesLength
?? 16)
584 if (params
?.upperCase
) {
585 return randomSerialNumberSuffix
.toUpperCase();
587 return randomSerialNumberSuffix
;