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 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(
114 stationTemplate
: ChargingStationTemplate
116 let configuredMaxConnectors
: number;
117 if (!Utils
.isEmptyArray(stationTemplate
.numberOfConnectors
)) {
118 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
119 // Distribute evenly the number of connectors
120 configuredMaxConnectors
= numberOfConnectors
[(index
- 1) % numberOfConnectors
.length
];
121 } else if (!Utils
.isUndefined(stationTemplate
.numberOfConnectors
)) {
122 configuredMaxConnectors
= stationTemplate
.numberOfConnectors
as number;
124 configuredMaxConnectors
= stationTemplate
?.Connectors
[0]
125 ? ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
) - 1
126 : ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
);
128 return configuredMaxConnectors
;
131 public static checkConfiguredMaxConnectors(
132 configuredMaxConnectors
: number,
133 templateFile
: string,
136 if (configuredMaxConnectors
<= 0) {
138 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
143 public static createBootNotificationRequest(
144 stationInfo
: ChargingStationInfo
145 ): BootNotificationRequest
{
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
,
169 public static workerPoolInUse(): boolean {
170 return [WorkerProcessType
.DYNAMIC_POOL
, WorkerProcessType
.STATIC_POOL
].includes(
171 Configuration
.getWorker().processType
175 public static workerDynamicPoolInUse(): boolean {
176 return Configuration
.getWorker().processType
=== WorkerProcessType
.DYNAMIC_POOL
;
180 * Convert websocket error code to human readable string message
182 * @param code websocket error code
183 * @returns human readable string message
185 public static getWebSocketCloseEventStatusString(code
: number): string {
186 if (code
>= 0 && code
<= 999) {
188 } else if (code
>= 1016) {
190 return '(For WebSocket standard)';
191 } else if (code
<= 2999) {
192 return '(For WebSocket extensions)';
193 } else if (code
<= 3999) {
194 return '(For libraries and frameworks)';
195 } else if (code
<= 4999) {
196 return '(For applications)';
199 if (!Utils
.isUndefined(WebSocketCloseEventStatusString
[code
])) {
200 return WebSocketCloseEventStatusString
[code
] as string;
205 public static warnDeprecatedTemplateKey(
206 template
: ChargingStationTemplate
,
208 templateFile
: string,
212 if (!Utils
.isUndefined(template
[key
])) {
214 `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
215 logMsgToAppend && '. ' + logMsgToAppend
221 public static convertDeprecatedTemplateKey(
222 template
: ChargingStationTemplate
,
223 deprecatedKey
: string,
226 if (!Utils
.isUndefined(template
[deprecatedKey
])) {
227 template
[key
] = template
[deprecatedKey
] as unknown
;
228 delete template
[deprecatedKey
];
232 public static stationTemplateToStationInfo(
233 stationTemplate
: ChargingStationTemplate
234 ): ChargingStationInfo
{
235 stationTemplate
= Utils
.cloneObject(stationTemplate
);
236 delete stationTemplate
.power
;
237 delete stationTemplate
.powerUnit
;
238 delete stationTemplate
.Configuration
;
239 delete stationTemplate
.AutomaticTransactionGenerator
;
240 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
241 delete stationTemplate
.chargePointSerialNumberPrefix
;
242 delete stationTemplate
.meterSerialNumberPrefix
;
243 return stationTemplate
;
246 public static createStationInfoHash(stationInfo
: ChargingStationInfo
): void {
247 delete stationInfo
.infoHash
;
248 stationInfo
.infoHash
= crypto
249 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
250 .update(JSON
.stringify(stationInfo
))
254 public static createSerialNumber(
255 stationTemplate
: ChargingStationTemplate
,
256 stationInfo
: ChargingStationInfo
= {} as ChargingStationInfo
,
258 randomSerialNumberUpperCase
?: boolean;
259 randomSerialNumber
?: boolean;
261 randomSerialNumberUpperCase
: true,
262 randomSerialNumber
: true,
265 params
= params
?? {};
266 params
.randomSerialNumberUpperCase
= params
?.randomSerialNumberUpperCase
?? true;
267 params
.randomSerialNumber
= params
?.randomSerialNumber
?? true;
268 const serialNumberSuffix
= params
?.randomSerialNumber
269 ? ChargingStationUtils
.getRandomSerialNumberSuffix({
270 upperCase
: params
.randomSerialNumberUpperCase
,
273 stationInfo
.chargePointSerialNumber
=
274 stationTemplate
?.chargePointSerialNumberPrefix
&&
275 stationTemplate
.chargePointSerialNumberPrefix
+ serialNumberSuffix
;
276 stationInfo
.chargeBoxSerialNumber
=
277 stationTemplate
?.chargeBoxSerialNumberPrefix
&&
278 stationTemplate
.chargeBoxSerialNumberPrefix
+ serialNumberSuffix
;
279 stationInfo
.meterSerialNumber
=
280 stationTemplate
?.meterSerialNumberPrefix
&&
281 stationTemplate
.meterSerialNumberPrefix
+ serialNumberSuffix
;
284 public static propagateSerialNumber(
285 stationTemplate
: ChargingStationTemplate
,
286 stationInfoSrc
: ChargingStationInfo
,
287 stationInfoDst
: ChargingStationInfo
= {} as ChargingStationInfo
289 if (!stationInfoSrc
|| !stationTemplate
) {
291 'Missing charging station template or existing configuration to propagate serial number'
294 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
295 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
296 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
297 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
298 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
299 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
300 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
301 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
302 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
305 public static getAmperageLimitationUnitDivider(stationInfo
: ChargingStationInfo
): number {
307 switch (stationInfo
.amperageLimitationUnit
) {
308 case AmpereUnits
.DECI_AMPERE
:
311 case AmpereUnits
.CENTI_AMPERE
:
314 case AmpereUnits
.MILLI_AMPERE
:
322 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
324 * @param {ChargingProfile[]} chargingProfiles
325 * @param {string} logPrefix
326 * @returns {{ limit, matchingChargingProfile }}
328 public static getLimitFromChargingProfiles(
329 chargingProfiles
: ChargingProfile
[],
333 matchingChargingProfile
: ChargingProfile
;
335 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
336 for (const chargingProfile
of chargingProfiles
) {
338 const currentMoment
= moment();
339 const chargingSchedule
= chargingProfile
.chargingSchedule
;
340 // Check type (recurring) and if it is already active
341 // Adjust the daily recurring schedule to today
343 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
344 chargingProfile
.recurrencyKind
=== RecurrencyKindType
.DAILY
&&
345 currentMoment
.isAfter(chargingSchedule
.startSchedule
)
347 const currentDate
= new Date();
348 chargingSchedule
.startSchedule
= new Date(chargingSchedule
.startSchedule
);
349 chargingSchedule
.startSchedule
.setFullYear(
350 currentDate
.getFullYear(),
351 currentDate
.getMonth(),
352 currentDate
.getDate()
354 // Check if the start of the schedule is yesterday
355 if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
356 chargingSchedule
.startSchedule
.setDate(currentDate
.getDate() - 1);
358 } else if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
361 // Check if the charging profile is active
363 moment(chargingSchedule
.startSchedule
)
364 .add(chargingSchedule
.duration
, 's')
365 .isAfter(currentMoment
)
367 let lastButOneSchedule
: ChargingSchedulePeriod
;
368 // Search the right schedule period
369 for (const schedulePeriod
of chargingSchedule
.chargingSchedulePeriod
) {
370 // Handling of only one period
372 chargingSchedule
.chargingSchedulePeriod
.length
=== 1 &&
373 schedulePeriod
.startPeriod
=== 0
376 limit
: schedulePeriod
.limit
,
377 matchingChargingProfile
: chargingProfile
,
379 logger
.debug(debugLogMsg
, result
);
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
,
393 logger
.debug(debugLogMsg
, result
);
397 lastButOneSchedule
= schedulePeriod
;
398 // Handle the last schedule period
400 schedulePeriod
.startPeriod
===
401 chargingSchedule
.chargingSchedulePeriod
[
402 chargingSchedule
.chargingSchedulePeriod
.length
- 1
406 limit
: lastButOneSchedule
.limit
,
407 matchingChargingProfile
: chargingProfile
,
409 logger
.debug(debugLogMsg
, result
);
418 public static getDefaultVoltageOut(
419 currentType
: CurrentType
,
420 templateFile
: string,
423 const errMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
424 let defaultVoltageOut
: number;
425 switch (currentType
) {
427 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
430 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
433 logger
.error(`${logPrefix} ${errMsg}`);
434 throw new BaseError(errMsg
);
436 return defaultVoltageOut
;
439 public static getSampledValueTemplate(
440 chargingStation
: ChargingStation
,
442 measurand
: MeterValueMeasurand
= MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
443 phase
?: MeterValuePhase
444 ): SampledValueTemplate
| undefined {
445 const onPhaseStr
= phase
? `on phase ${phase} ` : '';
446 if (!Constants
.SUPPORTED_MEASURANDS
.includes(measurand
)) {
448 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
453 measurand
!== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
454 !ChargingStationConfigurationUtils
.getConfigurationKey(
456 StandardParametersKey
.MeterValuesSampledData
457 )?.value
.includes(measurand
)
460 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId} not found in '${
461 StandardParametersKey.MeterValuesSampledData
466 const sampledValueTemplates
: SampledValueTemplate
[] =
467 chargingStation
.getConnectorStatus(connectorId
).MeterValues
;
470 !Utils
.isEmptyArray(sampledValueTemplates
) && index
< sampledValueTemplates
.length
;
474 !Constants
.SUPPORTED_MEASURANDS
.includes(
475 sampledValueTemplates
[index
]?.measurand
??
476 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
480 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
484 sampledValueTemplates
[index
]?.phase
=== phase
&&
485 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
486 ChargingStationConfigurationUtils
.getConfigurationKey(
488 StandardParametersKey
.MeterValuesSampledData
489 )?.value
.includes(measurand
)
491 return sampledValueTemplates
[index
];
494 !sampledValueTemplates
[index
].phase
&&
495 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
496 ChargingStationConfigurationUtils
.getConfigurationKey(
498 StandardParametersKey
.MeterValuesSampledData
499 )?.value
.includes(measurand
)
501 return sampledValueTemplates
[index
];
503 measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
504 (!sampledValueTemplates
[index
].measurand
||
505 sampledValueTemplates
[index
].measurand
=== measurand
)
507 return sampledValueTemplates
[index
];
510 if (measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
) {
511 const errorMsg
= `Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`;
512 logger
.error(`${chargingStation.logPrefix()} ${errorMsg}`);
513 throw new BaseError(errorMsg
);
516 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
520 public static getAuthorizationFile(stationInfo
: ChargingStationInfo
): string | undefined {
522 stationInfo
.authorizationFile
&&
524 path
.resolve(path
.dirname(fileURLToPath(import.meta
.url
)), '../'),
526 path
.basename(stationInfo
.authorizationFile
)
531 public static isRequestCommandSupported(
532 command
: RequestCommand
,
533 chargingStation
: ChargingStation
535 const isRequestCommand
= Object.values(RequestCommand
).includes(command
);
536 if (isRequestCommand
&& !chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
) {
538 } else if (isRequestCommand
&& chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
) {
539 return chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
[command
] ?? false;
541 logger
.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
545 public static isIncomingRequestCommandSupported(
546 command
: IncomingRequestCommand
,
547 chargingStation
: ChargingStation
549 const isIncomingRequestCommand
= Object.values(IncomingRequestCommand
).includes(command
);
551 isIncomingRequestCommand
&&
552 !chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
556 isIncomingRequestCommand
&&
557 chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
559 return chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
[command
] ?? false;
561 logger
.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
565 private static getRandomSerialNumberSuffix(params
?: {
566 randomBytesLength
?: number;
569 const randomSerialNumberSuffix
= crypto
570 .randomBytes(params
?.randomBytesLength
?? 16)
572 if (params
?.upperCase
) {
573 return randomSerialNumberSuffix
.toUpperCase();
575 return randomSerialNumberSuffix
;