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 { WorkerProcessType
} from
'../types/Worker';
25 import Configuration from
'../utils/Configuration';
26 import Constants from
'../utils/Constants';
27 import logger from
'../utils/Logger';
28 import Utils from
'../utils/Utils';
29 import type ChargingStation from
'./ChargingStation';
30 import { ChargingStationConfigurationUtils
} from
'./ChargingStationConfigurationUtils';
32 const moduleName
= 'ChargingStationUtils';
34 export class ChargingStationUtils
{
35 private constructor() {
36 // This is intentional
39 public static getChargingStationId(
41 stationTemplate
: ChargingStationTemplate
43 // In case of multiple instances: add instance index to charging station id
44 const instanceIndex
= process
.env
.CF_INSTANCE_INDEX
?? 0;
45 const idSuffix
= stationTemplate
.nameSuffix
?? '';
46 const idStr
= '000000000' + index
.toString();
47 return stationTemplate
?.fixedName
48 ? stationTemplate
.baseName
49 : stationTemplate
.baseName
+
51 instanceIndex
.toString() +
52 idStr
.substring(idStr
.length
- 4) +
56 public static getHashId(index
: number, stationTemplate
: ChargingStationTemplate
): string {
57 const hashBootNotificationRequest
= {
58 chargePointModel
: stationTemplate
.chargePointModel
,
59 chargePointVendor
: stationTemplate
.chargePointVendor
,
60 ...(!Utils
.isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
61 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
63 ...(!Utils
.isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
64 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
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(hashBootNotificationRequest
) +
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(
113 stationTemplate
: ChargingStationTemplate
115 let configuredMaxConnectors
: number;
116 if (!Utils
.isEmptyArray(stationTemplate
.numberOfConnectors
)) {
117 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
118 // Distribute evenly the number of connectors
119 configuredMaxConnectors
= numberOfConnectors
[(index
- 1) % numberOfConnectors
.length
];
120 } else if (!Utils
.isUndefined(stationTemplate
.numberOfConnectors
)) {
121 configuredMaxConnectors
= stationTemplate
.numberOfConnectors
as number;
123 configuredMaxConnectors
= stationTemplate
?.Connectors
[0]
124 ? ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
) - 1
125 : ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
);
127 return configuredMaxConnectors
;
130 public static checkConfiguredMaxConnectors(
131 configuredMaxConnectors
: number,
132 templateFile
: string,
135 if (configuredMaxConnectors
<= 0) {
137 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
142 public static createBootNotificationRequest(
143 stationInfo
: ChargingStationInfo
144 ): BootNotificationRequest
{
146 chargePointModel
: stationInfo
.chargePointModel
,
147 chargePointVendor
: stationInfo
.chargePointVendor
,
148 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
149 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
151 ...(!Utils
.isUndefined(stationInfo
.chargePointSerialNumber
) && {
152 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
154 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
155 firmwareVersion
: stationInfo
.firmwareVersion
,
157 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
158 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
159 ...(!Utils
.isUndefined(stationInfo
.meterSerialNumber
) && {
160 meterSerialNumber
: stationInfo
.meterSerialNumber
,
162 ...(!Utils
.isUndefined(stationInfo
.meterType
) && {
163 meterType
: stationInfo
.meterType
,
168 public static workerPoolInUse(): boolean {
169 return [WorkerProcessType
.DYNAMIC_POOL
, WorkerProcessType
.STATIC_POOL
].includes(
170 Configuration
.getWorker().processType
174 public static workerDynamicPoolInUse(): boolean {
175 return Configuration
.getWorker().processType
=== WorkerProcessType
.DYNAMIC_POOL
;
178 public static warnDeprecatedTemplateKey(
179 template
: ChargingStationTemplate
,
181 templateFile
: string,
185 if (!Utils
.isUndefined(template
[key
])) {
187 `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
188 logMsgToAppend && '. ' + logMsgToAppend
194 public static convertDeprecatedTemplateKey(
195 template
: ChargingStationTemplate
,
196 deprecatedKey
: string,
199 if (!Utils
.isUndefined(template
[deprecatedKey
])) {
200 template
[key
] = template
[deprecatedKey
] as unknown
;
201 delete template
[deprecatedKey
];
205 public static stationTemplateToStationInfo(
206 stationTemplate
: ChargingStationTemplate
207 ): ChargingStationInfo
{
208 stationTemplate
= Utils
.cloneObject(stationTemplate
);
209 delete stationTemplate
.power
;
210 delete stationTemplate
.powerUnit
;
211 delete stationTemplate
.Configuration
;
212 delete stationTemplate
.AutomaticTransactionGenerator
;
213 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
214 delete stationTemplate
.chargePointSerialNumberPrefix
;
215 delete stationTemplate
.meterSerialNumberPrefix
;
216 return stationTemplate
as unknown
as ChargingStationInfo
;
219 public static createStationInfoHash(stationInfo
: ChargingStationInfo
): void {
220 delete stationInfo
.infoHash
;
221 stationInfo
.infoHash
= crypto
222 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
223 .update(JSON
.stringify(stationInfo
))
227 public static createSerialNumber(
228 stationTemplate
: ChargingStationTemplate
,
229 stationInfo
: ChargingStationInfo
= {} as ChargingStationInfo
,
231 randomSerialNumberUpperCase
?: boolean;
232 randomSerialNumber
?: boolean;
234 randomSerialNumberUpperCase
: true,
235 randomSerialNumber
: true,
238 params
= params
?? {};
239 params
.randomSerialNumberUpperCase
= params
?.randomSerialNumberUpperCase
?? true;
240 params
.randomSerialNumber
= params
?.randomSerialNumber
?? true;
241 const serialNumberSuffix
= params
?.randomSerialNumber
242 ? ChargingStationUtils
.getRandomSerialNumberSuffix({
243 upperCase
: params
.randomSerialNumberUpperCase
,
246 stationInfo
.chargePointSerialNumber
=
247 stationTemplate
?.chargePointSerialNumberPrefix
&&
248 stationTemplate
.chargePointSerialNumberPrefix
+ serialNumberSuffix
;
249 stationInfo
.chargeBoxSerialNumber
=
250 stationTemplate
?.chargeBoxSerialNumberPrefix
&&
251 stationTemplate
.chargeBoxSerialNumberPrefix
+ serialNumberSuffix
;
252 stationInfo
.meterSerialNumber
=
253 stationTemplate
?.meterSerialNumberPrefix
&&
254 stationTemplate
.meterSerialNumberPrefix
+ serialNumberSuffix
;
257 public static propagateSerialNumber(
258 stationTemplate
: ChargingStationTemplate
,
259 stationInfoSrc
: ChargingStationInfo
,
260 stationInfoDst
: ChargingStationInfo
= {} as ChargingStationInfo
262 if (!stationInfoSrc
|| !stationTemplate
) {
264 'Missing charging station template or existing configuration to propagate serial number'
267 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
268 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
269 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
270 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
271 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
272 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
273 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
274 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
275 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
278 public static getAmperageLimitationUnitDivider(stationInfo
: ChargingStationInfo
): number {
280 switch (stationInfo
.amperageLimitationUnit
) {
281 case AmpereUnits
.DECI_AMPERE
:
284 case AmpereUnits
.CENTI_AMPERE
:
287 case AmpereUnits
.MILLI_AMPERE
:
295 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
297 * @param {ChargingProfile[]} chargingProfiles
298 * @param {string} logPrefix
299 * @returns {{ limit, matchingChargingProfile }}
301 public static getLimitFromChargingProfiles(
302 chargingProfiles
: ChargingProfile
[],
306 matchingChargingProfile
: ChargingProfile
;
308 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
309 for (const chargingProfile
of chargingProfiles
) {
311 const currentMoment
= moment();
312 const chargingSchedule
= chargingProfile
.chargingSchedule
;
313 // Check type (recurring) and if it is already active
314 // Adjust the daily recurring schedule to today
316 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
317 chargingProfile
.recurrencyKind
=== RecurrencyKindType
.DAILY
&&
318 currentMoment
.isAfter(chargingSchedule
.startSchedule
)
320 const currentDate
= new Date();
321 chargingSchedule
.startSchedule
= new Date(chargingSchedule
.startSchedule
);
322 chargingSchedule
.startSchedule
.setFullYear(
323 currentDate
.getFullYear(),
324 currentDate
.getMonth(),
325 currentDate
.getDate()
327 // Check if the start of the schedule is yesterday
328 if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
329 chargingSchedule
.startSchedule
.setDate(currentDate
.getDate() - 1);
331 } else if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
334 // Check if the charging profile is active
336 moment(chargingSchedule
.startSchedule
)
337 .add(chargingSchedule
.duration
, 's')
338 .isAfter(currentMoment
)
340 let lastButOneSchedule
: ChargingSchedulePeriod
;
341 // Search the right schedule period
342 for (const schedulePeriod
of chargingSchedule
.chargingSchedulePeriod
) {
343 // Handling of only one period
345 chargingSchedule
.chargingSchedulePeriod
.length
=== 1 &&
346 schedulePeriod
.startPeriod
=== 0
349 limit
: schedulePeriod
.limit
,
350 matchingChargingProfile
: chargingProfile
,
352 logger
.debug(debugLogMsg
, result
);
355 // Find the right schedule period
357 moment(chargingSchedule
.startSchedule
)
358 .add(schedulePeriod
.startPeriod
, 's')
359 .isAfter(currentMoment
)
361 // Found the schedule: last but one is the correct one
363 limit
: lastButOneSchedule
.limit
,
364 matchingChargingProfile
: chargingProfile
,
366 logger
.debug(debugLogMsg
, result
);
370 lastButOneSchedule
= schedulePeriod
;
371 // Handle the last schedule period
373 schedulePeriod
.startPeriod
===
374 chargingSchedule
.chargingSchedulePeriod
[
375 chargingSchedule
.chargingSchedulePeriod
.length
- 1
379 limit
: lastButOneSchedule
.limit
,
380 matchingChargingProfile
: chargingProfile
,
382 logger
.debug(debugLogMsg
, result
);
391 public static getDefaultVoltageOut(
392 currentType
: CurrentType
,
393 templateFile
: string,
396 const errMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
397 let defaultVoltageOut
: number;
398 switch (currentType
) {
400 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
403 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
406 logger
.error(`${logPrefix} ${errMsg}`);
407 throw new BaseError(errMsg
);
409 return defaultVoltageOut
;
412 public static getSampledValueTemplate(
413 chargingStation
: ChargingStation
,
415 measurand
: MeterValueMeasurand
= MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
416 phase
?: MeterValuePhase
417 ): SampledValueTemplate
| undefined {
418 const onPhaseStr
= phase
? `on phase ${phase} ` : '';
419 if (!Constants
.SUPPORTED_MEASURANDS
.includes(measurand
)) {
421 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
426 measurand
!== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
427 !ChargingStationConfigurationUtils
.getConfigurationKey(
429 StandardParametersKey
.MeterValuesSampledData
430 )?.value
.includes(measurand
)
433 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId} not found in '${
434 StandardParametersKey.MeterValuesSampledData
439 const sampledValueTemplates
: SampledValueTemplate
[] =
440 chargingStation
.getConnectorStatus(connectorId
).MeterValues
;
443 !Utils
.isEmptyArray(sampledValueTemplates
) && index
< sampledValueTemplates
.length
;
447 !Constants
.SUPPORTED_MEASURANDS
.includes(
448 sampledValueTemplates
[index
]?.measurand
??
449 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
453 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
457 sampledValueTemplates
[index
]?.phase
=== phase
&&
458 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
459 ChargingStationConfigurationUtils
.getConfigurationKey(
461 StandardParametersKey
.MeterValuesSampledData
462 )?.value
.includes(measurand
)
464 return sampledValueTemplates
[index
];
467 !sampledValueTemplates
[index
].phase
&&
468 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
469 ChargingStationConfigurationUtils
.getConfigurationKey(
471 StandardParametersKey
.MeterValuesSampledData
472 )?.value
.includes(measurand
)
474 return sampledValueTemplates
[index
];
476 measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
477 (!sampledValueTemplates
[index
].measurand
||
478 sampledValueTemplates
[index
].measurand
=== measurand
)
480 return sampledValueTemplates
[index
];
483 if (measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
) {
484 const errorMsg
= `Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`;
485 logger
.error(`${chargingStation.logPrefix()} ${errorMsg}`);
486 throw new BaseError(errorMsg
);
489 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
493 public static getAuthorizationFile(stationInfo
: ChargingStationInfo
): string | undefined {
495 stationInfo
.authorizationFile
&&
497 path
.resolve(path
.dirname(fileURLToPath(import.meta
.url
)), '../'),
499 path
.basename(stationInfo
.authorizationFile
)
504 public static isRequestCommandSupported(
505 command
: RequestCommand
,
506 chargingStation
: ChargingStation
508 const isRequestCommand
= Object.values(RequestCommand
).includes(command
);
509 if (isRequestCommand
&& !chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
) {
511 } else if (isRequestCommand
&& chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
) {
512 return chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
[command
] ?? false;
514 logger
.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
518 public static isIncomingRequestCommandSupported(
519 command
: IncomingRequestCommand
,
520 chargingStation
: ChargingStation
522 const isIncomingRequestCommand
= Object.values(IncomingRequestCommand
).includes(command
);
524 isIncomingRequestCommand
&&
525 !chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
529 isIncomingRequestCommand
&&
530 chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
532 return chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
[command
] ?? false;
534 logger
.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
538 private static getRandomSerialNumberSuffix(params
?: {
539 randomBytesLength
?: number;
542 const randomSerialNumberSuffix
= crypto
543 .randomBytes(params
?.randomBytesLength
?? 16)
545 if (params
?.upperCase
) {
546 return randomSerialNumberSuffix
.toUpperCase();
548 return randomSerialNumberSuffix
;