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 ChargingStationInfo from
'../types/ChargingStationInfo';
9 import ChargingStationTemplate
, {
13 } from
'../types/ChargingStationTemplate';
14 import { SampledValueTemplate
} from
'../types/MeasurandPerPhaseSampledValueTemplates';
15 import { ChargingProfileKindType
, RecurrencyKindType
} from
'../types/ocpp/1.6/ChargingProfile';
16 import { ChargingProfile
, ChargingSchedulePeriod
} from
'../types/ocpp/ChargingProfile';
17 import { StandardParametersKey
} from
'../types/ocpp/Configuration';
18 import { MeterValueMeasurand
, MeterValuePhase
} from
'../types/ocpp/MeterValues';
19 import { BootNotificationRequest
} from
'../types/ocpp/Requests';
20 import { WebSocketCloseEventStatusString
} from
'../types/WebSocket';
21 import { WorkerProcessType
} from
'../types/Worker';
22 import Configuration from
'../utils/Configuration';
23 import Constants from
'../utils/Constants';
24 import logger from
'../utils/Logger';
25 import Utils from
'../utils/Utils';
26 import ChargingStation from
'./ChargingStation';
27 import { ChargingStationConfigurationUtils
} from
'./ChargingStationConfigurationUtils';
29 export class ChargingStationUtils
{
30 private constructor() {
31 // This is intentional
34 public static getChargingStationId(
36 stationTemplate
: ChargingStationTemplate
38 // In case of multiple instances: add instance index to charging station id
39 const instanceIndex
= process
.env
.CF_INSTANCE_INDEX
?? 0;
40 const idSuffix
= stationTemplate
.nameSuffix
?? '';
41 const idStr
= '000000000' + index
.toString();
42 return stationTemplate
?.fixedName
43 ? stationTemplate
.baseName
44 : stationTemplate
.baseName
+
46 instanceIndex
.toString() +
47 idStr
.substring(idStr
.length
- 4) +
51 public static getHashId(index
: number, stationTemplate
: ChargingStationTemplate
): string {
52 const hashBootNotificationRequest
= {
53 chargePointModel
: stationTemplate
.chargePointModel
,
54 chargePointVendor
: stationTemplate
.chargePointVendor
,
55 ...(!Utils
.isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
56 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
58 ...(!Utils
.isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
59 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
61 ...(!Utils
.isUndefined(stationTemplate
.firmwareVersion
) && {
62 firmwareVersion
: stationTemplate
.firmwareVersion
,
64 ...(!Utils
.isUndefined(stationTemplate
.iccid
) && { iccid
: stationTemplate
.iccid
}),
65 ...(!Utils
.isUndefined(stationTemplate
.imsi
) && { imsi
: stationTemplate
.imsi
}),
66 ...(!Utils
.isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
67 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
69 ...(!Utils
.isUndefined(stationTemplate
.meterType
) && {
70 meterType
: stationTemplate
.meterType
,
74 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
76 JSON
.stringify(hashBootNotificationRequest
) +
77 ChargingStationUtils
.getChargingStationId(index
, stationTemplate
)
82 public static getTemplateMaxNumberOfConnectors(stationTemplate
: ChargingStationTemplate
): number {
83 const templateConnectors
= stationTemplate
?.Connectors
;
84 if (!templateConnectors
) {
87 return Object.keys(templateConnectors
).length
;
90 public static checkTemplateMaxConnectors(
91 templateMaxConnectors
: number,
95 if (templateMaxConnectors
=== 0) {
97 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
99 } else if (templateMaxConnectors
< 0) {
101 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
106 public static getConfiguredNumberOfConnectors(
108 stationTemplate
: ChargingStationTemplate
110 let configuredMaxConnectors
: number;
111 if (!Utils
.isEmptyArray(stationTemplate
.numberOfConnectors
)) {
112 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
113 // Distribute evenly the number of connectors
114 configuredMaxConnectors
= numberOfConnectors
[(index
- 1) % numberOfConnectors
.length
];
115 } else if (!Utils
.isUndefined(stationTemplate
.numberOfConnectors
)) {
116 configuredMaxConnectors
= stationTemplate
.numberOfConnectors
as number;
118 configuredMaxConnectors
= stationTemplate
?.Connectors
[0]
119 ? ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
) - 1
120 : ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
);
122 return configuredMaxConnectors
;
125 public static checkConfiguredMaxConnectors(
126 configuredMaxConnectors
: number,
127 templateFile
: string,
130 if (configuredMaxConnectors
<= 0) {
132 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
137 public static createBootNotificationRequest(
138 stationInfo
: ChargingStationInfo
139 ): BootNotificationRequest
{
141 chargePointModel
: stationInfo
.chargePointModel
,
142 chargePointVendor
: stationInfo
.chargePointVendor
,
143 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
144 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
146 ...(!Utils
.isUndefined(stationInfo
.chargePointSerialNumber
) && {
147 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
149 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
150 firmwareVersion
: stationInfo
.firmwareVersion
,
152 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
153 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
154 ...(!Utils
.isUndefined(stationInfo
.meterSerialNumber
) && {
155 meterSerialNumber
: stationInfo
.meterSerialNumber
,
157 ...(!Utils
.isUndefined(stationInfo
.meterType
) && {
158 meterType
: stationInfo
.meterType
,
163 public static workerPoolInUse(): boolean {
164 return [WorkerProcessType
.DYNAMIC_POOL
, WorkerProcessType
.STATIC_POOL
].includes(
165 Configuration
.getWorker().processType
169 public static workerDynamicPoolInUse(): boolean {
170 return Configuration
.getWorker().processType
=== WorkerProcessType
.DYNAMIC_POOL
;
174 * Convert websocket error code to human readable string message
176 * @param code websocket error code
177 * @returns human readable string message
179 public static getWebSocketCloseEventStatusString(code
: number): string {
180 if (code
>= 0 && code
<= 999) {
182 } else if (code
>= 1016) {
184 return '(For WebSocket standard)';
185 } else if (code
<= 2999) {
186 return '(For WebSocket extensions)';
187 } else if (code
<= 3999) {
188 return '(For libraries and frameworks)';
189 } else if (code
<= 4999) {
190 return '(For applications)';
193 if (!Utils
.isUndefined(WebSocketCloseEventStatusString
[code
])) {
194 return WebSocketCloseEventStatusString
[code
] as string;
199 public static warnDeprecatedTemplateKey(
200 template
: ChargingStationTemplate
,
202 templateFile
: string,
206 if (!Utils
.isUndefined(template
[key
])) {
208 `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
209 logMsgToAppend && '. ' + logMsgToAppend
215 public static convertDeprecatedTemplateKey(
216 template
: ChargingStationTemplate
,
217 deprecatedKey
: string,
220 if (!Utils
.isUndefined(template
[deprecatedKey
])) {
221 template
[key
] = template
[deprecatedKey
] as unknown
;
222 delete template
[deprecatedKey
];
226 public static stationTemplateToStationInfo(
227 stationTemplate
: ChargingStationTemplate
228 ): ChargingStationInfo
{
229 stationTemplate
= Utils
.cloneObject(stationTemplate
);
230 delete stationTemplate
.power
;
231 delete stationTemplate
.powerUnit
;
232 delete stationTemplate
.Configuration
;
233 delete stationTemplate
.AutomaticTransactionGenerator
;
234 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
235 delete stationTemplate
.chargePointSerialNumberPrefix
;
236 delete stationTemplate
.meterSerialNumberPrefix
;
237 return stationTemplate
;
240 public static createStationInfoHash(stationInfo
: ChargingStationInfo
): void {
241 delete stationInfo
.infoHash
;
242 stationInfo
.infoHash
= crypto
243 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
244 .update(JSON
.stringify(stationInfo
))
248 public static createSerialNumber(
249 stationTemplate
: ChargingStationTemplate
,
250 stationInfo
: ChargingStationInfo
= {} as ChargingStationInfo
,
252 randomSerialNumberUpperCase
?: boolean;
253 randomSerialNumber
?: boolean;
255 randomSerialNumberUpperCase
: true,
256 randomSerialNumber
: true,
259 params
= params
?? {};
260 params
.randomSerialNumberUpperCase
= params
?.randomSerialNumberUpperCase
?? true;
261 params
.randomSerialNumber
= params
?.randomSerialNumber
?? true;
262 const serialNumberSuffix
= params
?.randomSerialNumber
263 ? ChargingStationUtils
.getRandomSerialNumberSuffix({
264 upperCase
: params
.randomSerialNumberUpperCase
,
267 stationInfo
.chargePointSerialNumber
=
268 stationTemplate
?.chargePointSerialNumberPrefix
&&
269 stationTemplate
.chargePointSerialNumberPrefix
+ serialNumberSuffix
;
270 stationInfo
.chargeBoxSerialNumber
=
271 stationTemplate
?.chargeBoxSerialNumberPrefix
&&
272 stationTemplate
.chargeBoxSerialNumberPrefix
+ serialNumberSuffix
;
273 stationInfo
.meterSerialNumber
=
274 stationTemplate
?.meterSerialNumberPrefix
&&
275 stationTemplate
.meterSerialNumberPrefix
+ serialNumberSuffix
;
278 public static propagateSerialNumber(
279 stationTemplate
: ChargingStationTemplate
,
280 stationInfoSrc
: ChargingStationInfo
,
281 stationInfoDst
: ChargingStationInfo
= {} as ChargingStationInfo
283 if (!stationInfoSrc
|| !stationTemplate
) {
285 'Missing charging station template or existing configuration to propagate serial number'
288 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
289 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
290 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
291 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
292 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
293 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
294 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
295 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
296 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
299 public static getAmperageLimitationUnitDivider(stationInfo
: ChargingStationInfo
): number {
301 switch (stationInfo
.amperageLimitationUnit
) {
302 case AmpereUnits
.DECI_AMPERE
:
305 case AmpereUnits
.CENTI_AMPERE
:
308 case AmpereUnits
.MILLI_AMPERE
:
316 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
318 * @param {ChargingProfile[]} chargingProfiles
319 * @param {string} logPrefix
320 * @returns {{ limit, matchingChargingProfile }}
322 public static getLimitFromChargingProfiles(
323 chargingProfiles
: ChargingProfile
[],
327 matchingChargingProfile
: ChargingProfile
;
329 for (const chargingProfile
of chargingProfiles
) {
331 const currentMoment
= moment();
332 const chargingSchedule
= chargingProfile
.chargingSchedule
;
333 // Check type (recurring) and if it is already active
334 // Adjust the daily recurring schedule to today
336 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
337 chargingProfile
.recurrencyKind
=== RecurrencyKindType
.DAILY
&&
338 currentMoment
.isAfter(chargingSchedule
.startSchedule
)
340 const currentDate
= new Date();
341 chargingSchedule
.startSchedule
= new Date(chargingSchedule
.startSchedule
);
342 chargingSchedule
.startSchedule
.setFullYear(
343 currentDate
.getFullYear(),
344 currentDate
.getMonth(),
345 currentDate
.getDate()
347 // Check if the start of the schedule is yesterday
348 if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
349 chargingSchedule
.startSchedule
.setDate(currentDate
.getDate() - 1);
351 } else if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
354 // Check if the charging profile is active
356 moment(chargingSchedule
.startSchedule
)
357 .add(chargingSchedule
.duration
, 's')
358 .isAfter(currentMoment
)
360 let lastButOneSchedule
: ChargingSchedulePeriod
;
361 // Search the right schedule period
362 for (const schedulePeriod
of chargingSchedule
.chargingSchedulePeriod
) {
363 // Handling of only one period
365 chargingSchedule
.chargingSchedulePeriod
.length
=== 1 &&
366 schedulePeriod
.startPeriod
=== 0
369 limit
: schedulePeriod
.limit
,
370 matchingChargingProfile
: chargingProfile
,
373 `${logPrefix} Matching charging profile found for power limitation: %j`,
378 // Find the right schedule period
380 moment(chargingSchedule
.startSchedule
)
381 .add(schedulePeriod
.startPeriod
, 's')
382 .isAfter(currentMoment
)
384 // Found the schedule: last but one is the correct one
386 limit
: lastButOneSchedule
.limit
,
387 matchingChargingProfile
: chargingProfile
,
390 `${logPrefix} Matching charging profile found for power limitation: %j`,
396 lastButOneSchedule
= schedulePeriod
;
397 // Handle the last schedule period
399 schedulePeriod
.startPeriod
===
400 chargingSchedule
.chargingSchedulePeriod
[
401 chargingSchedule
.chargingSchedulePeriod
.length
- 1
405 limit
: lastButOneSchedule
.limit
,
406 matchingChargingProfile
: chargingProfile
,
409 `${logPrefix} Matching charging profile found for power limitation: %j`,
420 public static getDefaultVoltageOut(
421 currentType
: CurrentType
,
422 templateFile
: string,
425 const errMsg
= `${logPrefix} Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
426 let defaultVoltageOut
: number;
427 switch (currentType
) {
429 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
432 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
435 logger
.error(errMsg
);
436 throw new Error(errMsg
);
438 return defaultVoltageOut
;
441 public static getSampledValueTemplate(
442 chargingStation
: ChargingStation
,
444 measurand
: MeterValueMeasurand
= MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
445 phase
?: MeterValuePhase
446 ): SampledValueTemplate
| undefined {
447 const onPhaseStr
= phase
? `on phase ${phase} ` : '';
448 if (!Constants
.SUPPORTED_MEASURANDS
.includes(measurand
)) {
450 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
455 measurand
!== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
456 !ChargingStationConfigurationUtils
.getConfigurationKey(
458 StandardParametersKey
.MeterValuesSampledData
459 )?.value
.includes(measurand
)
462 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId} not found in '${
463 StandardParametersKey.MeterValuesSampledData
468 const sampledValueTemplates
: SampledValueTemplate
[] =
469 chargingStation
.getConnectorStatus(connectorId
).MeterValues
;
472 !Utils
.isEmptyArray(sampledValueTemplates
) && index
< sampledValueTemplates
.length
;
476 !Constants
.SUPPORTED_MEASURANDS
.includes(
477 sampledValueTemplates
[index
]?.measurand
??
478 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
482 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
486 sampledValueTemplates
[index
]?.phase
=== phase
&&
487 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
488 ChargingStationConfigurationUtils
.getConfigurationKey(
490 StandardParametersKey
.MeterValuesSampledData
491 )?.value
.includes(measurand
)
493 return sampledValueTemplates
[index
];
496 !sampledValueTemplates
[index
].phase
&&
497 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
498 ChargingStationConfigurationUtils
.getConfigurationKey(
500 StandardParametersKey
.MeterValuesSampledData
501 )?.value
.includes(measurand
)
503 return sampledValueTemplates
[index
];
505 measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
506 (!sampledValueTemplates
[index
].measurand
||
507 sampledValueTemplates
[index
].measurand
=== measurand
)
509 return sampledValueTemplates
[index
];
512 if (measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
) {
513 const errorMsg
= `${chargingStation.logPrefix()} Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`;
514 logger
.error(errorMsg
);
515 throw new Error(errorMsg
);
518 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
522 public static getAuthorizationFile(stationInfo
: ChargingStationInfo
): string | undefined {
524 stationInfo
.authorizationFile
&&
526 path
.resolve(path
.dirname(fileURLToPath(import.meta
.url
)), '../'),
528 path
.basename(stationInfo
.authorizationFile
)
533 private static getRandomSerialNumberSuffix(params
?: {
534 randomBytesLength
?: number;
537 const randomSerialNumberSuffix
= crypto
538 .randomBytes(params
?.randomBytesLength
?? 16)
540 if (params
?.upperCase
) {
541 return randomSerialNumberSuffix
.toUpperCase();
543 return randomSerialNumberSuffix
;