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';
9 import ChargingStationTemplate
, {
13 } from
'../types/ChargingStationTemplate';
14 import type { SampledValueTemplate
} from
'../types/MeasurandPerPhaseSampledValueTemplates';
15 import { ChargingProfileKindType
, RecurrencyKindType
} from
'../types/ocpp/1.6/ChargingProfile';
16 import type { ChargingProfile
, ChargingSchedulePeriod
} from
'../types/ocpp/ChargingProfile';
17 import { StandardParametersKey
} from
'../types/ocpp/Configuration';
18 import { MeterValueMeasurand
, MeterValuePhase
} from
'../types/ocpp/MeterValues';
20 BootNotificationRequest
,
21 IncomingRequestCommand
,
23 } from
'../types/ocpp/Requests';
24 import { WebSocketCloseEventStatusString
} from
'../types/WebSocket';
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 export class ChargingStationUtils
{
34 private constructor() {
35 // This is intentional
38 public static getChargingStationId(
40 stationTemplate
: ChargingStationTemplate
42 // In case of multiple instances: add instance index to charging station id
43 const instanceIndex
= process
.env
.CF_INSTANCE_INDEX
?? 0;
44 const idSuffix
= stationTemplate
.nameSuffix
?? '';
45 const idStr
= '000000000' + index
.toString();
46 return stationTemplate
?.fixedName
47 ? stationTemplate
.baseName
48 : stationTemplate
.baseName
+
50 instanceIndex
.toString() +
51 idStr
.substring(idStr
.length
- 4) +
55 public static getHashId(index
: number, stationTemplate
: ChargingStationTemplate
): string {
56 const hashBootNotificationRequest
= {
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 ...(!Utils
.isUndefined(stationTemplate
.firmwareVersion
) && {
66 firmwareVersion
: stationTemplate
.firmwareVersion
,
68 ...(!Utils
.isUndefined(stationTemplate
.iccid
) && { iccid
: stationTemplate
.iccid
}),
69 ...(!Utils
.isUndefined(stationTemplate
.imsi
) && { imsi
: stationTemplate
.imsi
}),
70 ...(!Utils
.isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
71 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
73 ...(!Utils
.isUndefined(stationTemplate
.meterType
) && {
74 meterType
: stationTemplate
.meterType
,
78 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
80 JSON
.stringify(hashBootNotificationRequest
) +
81 ChargingStationUtils
.getChargingStationId(index
, stationTemplate
)
86 public static getTemplateMaxNumberOfConnectors(stationTemplate
: ChargingStationTemplate
): number {
87 const templateConnectors
= stationTemplate
?.Connectors
;
88 if (!templateConnectors
) {
91 return Object.keys(templateConnectors
).length
;
94 public static checkTemplateMaxConnectors(
95 templateMaxConnectors
: number,
99 if (templateMaxConnectors
=== 0) {
101 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
103 } else if (templateMaxConnectors
< 0) {
105 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
110 public static getConfiguredNumberOfConnectors(
112 stationTemplate
: ChargingStationTemplate
114 let configuredMaxConnectors
: number;
115 if (!Utils
.isEmptyArray(stationTemplate
.numberOfConnectors
)) {
116 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
117 // Distribute evenly the number of connectors
118 configuredMaxConnectors
= numberOfConnectors
[(index
- 1) % numberOfConnectors
.length
];
119 } else if (!Utils
.isUndefined(stationTemplate
.numberOfConnectors
)) {
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 ): BootNotificationRequest
{
145 chargePointModel
: stationInfo
.chargePointModel
,
146 chargePointVendor
: stationInfo
.chargePointVendor
,
147 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
148 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
150 ...(!Utils
.isUndefined(stationInfo
.chargePointSerialNumber
) && {
151 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
153 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
154 firmwareVersion
: stationInfo
.firmwareVersion
,
156 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
157 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
158 ...(!Utils
.isUndefined(stationInfo
.meterSerialNumber
) && {
159 meterSerialNumber
: stationInfo
.meterSerialNumber
,
161 ...(!Utils
.isUndefined(stationInfo
.meterType
) && {
162 meterType
: stationInfo
.meterType
,
167 public static workerPoolInUse(): boolean {
168 return [WorkerProcessType
.DYNAMIC_POOL
, WorkerProcessType
.STATIC_POOL
].includes(
169 Configuration
.getWorker().processType
173 public static workerDynamicPoolInUse(): boolean {
174 return Configuration
.getWorker().processType
=== WorkerProcessType
.DYNAMIC_POOL
;
178 * Convert websocket error code to human readable string message
180 * @param code websocket error code
181 * @returns human readable string message
183 public static getWebSocketCloseEventStatusString(code
: number): string {
184 if (code
>= 0 && code
<= 999) {
186 } else if (code
>= 1016) {
188 return '(For WebSocket standard)';
189 } else if (code
<= 2999) {
190 return '(For WebSocket extensions)';
191 } else if (code
<= 3999) {
192 return '(For libraries and frameworks)';
193 } else if (code
<= 4999) {
194 return '(For applications)';
197 if (!Utils
.isUndefined(WebSocketCloseEventStatusString
[code
])) {
198 return WebSocketCloseEventStatusString
[code
] as string;
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 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
;
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
= {} as 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
=
272 stationTemplate
?.chargePointSerialNumberPrefix
&&
273 stationTemplate
.chargePointSerialNumberPrefix
+ serialNumberSuffix
;
274 stationInfo
.chargeBoxSerialNumber
=
275 stationTemplate
?.chargeBoxSerialNumberPrefix
&&
276 stationTemplate
.chargeBoxSerialNumberPrefix
+ serialNumberSuffix
;
277 stationInfo
.meterSerialNumber
=
278 stationTemplate
?.meterSerialNumberPrefix
&&
279 stationTemplate
.meterSerialNumberPrefix
+ serialNumberSuffix
;
282 public static propagateSerialNumber(
283 stationTemplate
: ChargingStationTemplate
,
284 stationInfoSrc
: ChargingStationInfo
,
285 stationInfoDst
: ChargingStationInfo
= {} as ChargingStationInfo
287 if (!stationInfoSrc
|| !stationTemplate
) {
289 'Missing charging station template or existing configuration to propagate serial number'
292 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
293 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
294 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
295 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
296 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
297 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
298 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
299 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
300 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
303 public static getAmperageLimitationUnitDivider(stationInfo
: ChargingStationInfo
): number {
305 switch (stationInfo
.amperageLimitationUnit
) {
306 case AmpereUnits
.DECI_AMPERE
:
309 case AmpereUnits
.CENTI_AMPERE
:
312 case AmpereUnits
.MILLI_AMPERE
:
320 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
322 * @param {ChargingProfile[]} chargingProfiles
323 * @param {string} logPrefix
324 * @returns {{ limit, matchingChargingProfile }}
326 public static getLimitFromChargingProfiles(
327 chargingProfiles
: ChargingProfile
[],
331 matchingChargingProfile
: ChargingProfile
;
333 for (const chargingProfile
of chargingProfiles
) {
335 const currentMoment
= moment();
336 const chargingSchedule
= chargingProfile
.chargingSchedule
;
337 // Check type (recurring) and if it is already active
338 // Adjust the daily recurring schedule to today
340 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
341 chargingProfile
.recurrencyKind
=== RecurrencyKindType
.DAILY
&&
342 currentMoment
.isAfter(chargingSchedule
.startSchedule
)
344 const currentDate
= new Date();
345 chargingSchedule
.startSchedule
= new Date(chargingSchedule
.startSchedule
);
346 chargingSchedule
.startSchedule
.setFullYear(
347 currentDate
.getFullYear(),
348 currentDate
.getMonth(),
349 currentDate
.getDate()
351 // Check if the start of the schedule is yesterday
352 if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
353 chargingSchedule
.startSchedule
.setDate(currentDate
.getDate() - 1);
355 } else if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
358 // Check if the charging profile is active
360 moment(chargingSchedule
.startSchedule
)
361 .add(chargingSchedule
.duration
, 's')
362 .isAfter(currentMoment
)
364 let lastButOneSchedule
: ChargingSchedulePeriod
;
365 // Search the right schedule period
366 for (const schedulePeriod
of chargingSchedule
.chargingSchedulePeriod
) {
367 // Handling of only one period
369 chargingSchedule
.chargingSchedulePeriod
.length
=== 1 &&
370 schedulePeriod
.startPeriod
=== 0
373 limit
: schedulePeriod
.limit
,
374 matchingChargingProfile
: chargingProfile
,
377 `${logPrefix} Matching charging profile found for power limitation: %j`,
382 // Find the right schedule period
384 moment(chargingSchedule
.startSchedule
)
385 .add(schedulePeriod
.startPeriod
, 's')
386 .isAfter(currentMoment
)
388 // Found the schedule: last but one is the correct one
390 limit
: lastButOneSchedule
.limit
,
391 matchingChargingProfile
: chargingProfile
,
394 `${logPrefix} Matching charging profile found for power limitation: %j`,
400 lastButOneSchedule
= schedulePeriod
;
401 // Handle the last schedule period
403 schedulePeriod
.startPeriod
===
404 chargingSchedule
.chargingSchedulePeriod
[
405 chargingSchedule
.chargingSchedulePeriod
.length
- 1
409 limit
: lastButOneSchedule
.limit
,
410 matchingChargingProfile
: chargingProfile
,
413 `${logPrefix} Matching charging profile found for power limitation: %j`,
424 public static getDefaultVoltageOut(
425 currentType
: CurrentType
,
426 templateFile
: string,
429 const errMsg
= `${logPrefix} 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(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
)) {
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
)
497 return sampledValueTemplates
[index
];
500 !sampledValueTemplates
[index
].phase
&&
501 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
502 ChargingStationConfigurationUtils
.getConfigurationKey(
504 StandardParametersKey
.MeterValuesSampledData
505 )?.value
.includes(measurand
)
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
= `${chargingStation.logPrefix()} Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`;
518 logger
.error(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
);
542 if (isRequestCommand
&& !chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
) {
544 } else if (isRequestCommand
&& chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
) {
545 return chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
[command
] ?? false;
547 logger
.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
551 public static isIncomingRequestCommandSupported(
552 command
: IncomingRequestCommand
,
553 chargingStation
: ChargingStation
555 const isIncomingRequestCommand
= Object.values(IncomingRequestCommand
).includes(command
);
557 isIncomingRequestCommand
&&
558 !chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
562 isIncomingRequestCommand
&&
563 chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
565 return chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
[command
] ?? false;
567 logger
.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
571 private static getRandomSerialNumberSuffix(params
?: {
572 randomBytesLength
?: number;
575 const randomSerialNumberSuffix
= crypto
576 .randomBytes(params
?.randomBytesLength
?? 16)
578 if (params
?.upperCase
) {
579 return randomSerialNumberSuffix
.toUpperCase();
581 return randomSerialNumberSuffix
;