1 import { ChargingProfile
, ChargingSchedulePeriod
} from
'../types/ocpp/ChargingProfile';
2 import { ChargingProfileKindType
, RecurrencyKindType
} from
'../types/ocpp/1.6/ChargingProfile';
3 import ChargingStationTemplate
, {
7 } from
'../types/ChargingStationTemplate';
8 import { MeterValueMeasurand
, MeterValuePhase
} from
'../types/ocpp/MeterValues';
10 import BaseError from
'../exception/BaseError';
11 import { BootNotificationRequest
} from
'../types/ocpp/Requests';
12 import ChargingStation from
'./ChargingStation';
13 import { ChargingStationConfigurationUtils
} from
'./ChargingStationConfigurationUtils';
14 import ChargingStationInfo from
'../types/ChargingStationInfo';
15 import Configuration from
'../utils/Configuration';
16 import Constants from
'../utils/Constants';
17 import { SampledValueTemplate
} from
'../types/MeasurandPerPhaseSampledValueTemplates';
18 import { StandardParametersKey
} from
'../types/ocpp/Configuration';
19 import Utils from
'../utils/Utils';
20 import { WebSocketCloseEventStatusString
} from
'../types/WebSocket';
21 import { WorkerProcessType
} from
'../types/Worker';
22 import crypto from
'crypto';
23 import { fileURLToPath
} from
'url';
24 import logger from
'../utils/Logger';
25 import moment from
'moment';
26 import path from
'path';
28 export class ChargingStationUtils
{
29 private constructor() {
30 // This is intentional
33 public static getChargingStationId(
35 stationTemplate
: ChargingStationTemplate
37 // In case of multiple instances: add instance index to charging station id
38 const instanceIndex
= process
.env
.CF_INSTANCE_INDEX
?? 0;
39 const idSuffix
= stationTemplate
.nameSuffix
?? '';
40 const idStr
= '000000000' + index
.toString();
41 return stationTemplate
?.fixedName
42 ? stationTemplate
.baseName
43 : stationTemplate
.baseName
+
45 instanceIndex
.toString() +
46 idStr
.substring(idStr
.length
- 4) +
50 public static getHashId(index
: number, stationTemplate
: ChargingStationTemplate
): string {
51 const hashBootNotificationRequest
= {
52 chargePointModel
: stationTemplate
.chargePointModel
,
53 chargePointVendor
: stationTemplate
.chargePointVendor
,
54 ...(!Utils
.isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
55 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
57 ...(!Utils
.isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
58 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
60 ...(!Utils
.isUndefined(stationTemplate
.firmwareVersion
) && {
61 firmwareVersion
: stationTemplate
.firmwareVersion
,
63 ...(!Utils
.isUndefined(stationTemplate
.iccid
) && { iccid
: stationTemplate
.iccid
}),
64 ...(!Utils
.isUndefined(stationTemplate
.imsi
) && { imsi
: stationTemplate
.imsi
}),
65 ...(!Utils
.isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
66 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
68 ...(!Utils
.isUndefined(stationTemplate
.meterType
) && {
69 meterType
: stationTemplate
.meterType
,
73 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
75 JSON
.stringify(hashBootNotificationRequest
) +
76 ChargingStationUtils
.getChargingStationId(index
, stationTemplate
)
81 public static getTemplateMaxNumberOfConnectors(stationTemplate
: ChargingStationTemplate
): number {
82 const templateConnectors
= stationTemplate
?.Connectors
;
83 if (!templateConnectors
) {
86 return Object.keys(templateConnectors
).length
;
89 public static checkTemplateMaxConnectors(
90 templateMaxConnectors
: number,
94 if (templateMaxConnectors
=== 0) {
96 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
98 } else if (templateMaxConnectors
< 0) {
100 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
105 public static getConfiguredNumberOfConnectors(
107 stationTemplate
: ChargingStationTemplate
109 let configuredMaxConnectors
: number;
110 if (!Utils
.isEmptyArray(stationTemplate
.numberOfConnectors
)) {
111 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
112 // Distribute evenly the number of connectors
113 configuredMaxConnectors
= numberOfConnectors
[(index
- 1) % numberOfConnectors
.length
];
114 } else if (!Utils
.isUndefined(stationTemplate
.numberOfConnectors
)) {
115 configuredMaxConnectors
= stationTemplate
.numberOfConnectors
as number;
117 configuredMaxConnectors
= stationTemplate
?.Connectors
[0]
118 ? ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
) - 1
119 : ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
);
121 return configuredMaxConnectors
;
124 public static checkConfiguredMaxConnectors(
125 configuredMaxConnectors
: number,
126 templateFile
: string,
129 if (configuredMaxConnectors
<= 0) {
131 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
136 public static createBootNotificationRequest(
137 stationInfo
: ChargingStationInfo
138 ): BootNotificationRequest
{
140 chargePointModel
: stationInfo
.chargePointModel
,
141 chargePointVendor
: stationInfo
.chargePointVendor
,
142 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
143 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
145 ...(!Utils
.isUndefined(stationInfo
.chargePointSerialNumber
) && {
146 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
148 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
149 firmwareVersion
: stationInfo
.firmwareVersion
,
151 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
152 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
153 ...(!Utils
.isUndefined(stationInfo
.meterSerialNumber
) && {
154 meterSerialNumber
: stationInfo
.meterSerialNumber
,
156 ...(!Utils
.isUndefined(stationInfo
.meterType
) && {
157 meterType
: stationInfo
.meterType
,
162 public static workerPoolInUse(): boolean {
163 return [WorkerProcessType
.DYNAMIC_POOL
, WorkerProcessType
.STATIC_POOL
].includes(
164 Configuration
.getWorker().processType
168 public static workerDynamicPoolInUse(): boolean {
169 return Configuration
.getWorker().processType
=== WorkerProcessType
.DYNAMIC_POOL
;
173 * Convert websocket error code to human readable string message
175 * @param code websocket error code
176 * @returns human readable string message
178 public static getWebSocketCloseEventStatusString(code
: number): string {
179 if (code
>= 0 && code
<= 999) {
181 } else if (code
>= 1016) {
183 return '(For WebSocket standard)';
184 } else if (code
<= 2999) {
185 return '(For WebSocket extensions)';
186 } else if (code
<= 3999) {
187 return '(For libraries and frameworks)';
188 } else if (code
<= 4999) {
189 return '(For applications)';
192 if (!Utils
.isUndefined(WebSocketCloseEventStatusString
[code
])) {
193 return WebSocketCloseEventStatusString
[code
] as string;
198 public static warnDeprecatedTemplateKey(
199 template
: ChargingStationTemplate
,
201 templateFile
: string,
205 if (!Utils
.isUndefined(template
[key
])) {
207 `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
208 logMsgToAppend && '. ' + logMsgToAppend
214 public static convertDeprecatedTemplateKey(
215 template
: ChargingStationTemplate
,
216 deprecatedKey
: string,
219 if (!Utils
.isUndefined(template
[deprecatedKey
])) {
220 template
[key
] = template
[deprecatedKey
] as unknown
;
221 delete template
[deprecatedKey
];
225 public static stationTemplateToStationInfo(
226 stationTemplate
: ChargingStationTemplate
227 ): ChargingStationInfo
{
228 stationTemplate
= Utils
.cloneObject(stationTemplate
);
229 delete stationTemplate
.power
;
230 delete stationTemplate
.powerUnit
;
231 delete stationTemplate
.Configuration
;
232 delete stationTemplate
.AutomaticTransactionGenerator
;
233 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
234 delete stationTemplate
.chargePointSerialNumberPrefix
;
235 delete stationTemplate
.meterSerialNumberPrefix
;
236 return stationTemplate
;
239 public static createStationInfoHash(stationInfo
: ChargingStationInfo
): void {
240 delete stationInfo
.infoHash
;
241 stationInfo
.infoHash
= crypto
242 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
243 .update(JSON
.stringify(stationInfo
))
247 public static createSerialNumber(
248 stationTemplate
: ChargingStationTemplate
,
249 stationInfo
: ChargingStationInfo
= {} as ChargingStationInfo
,
251 randomSerialNumberUpperCase
?: boolean;
252 randomSerialNumber
?: boolean;
254 randomSerialNumberUpperCase
: true,
255 randomSerialNumber
: true,
258 params
= params
?? {};
259 params
.randomSerialNumberUpperCase
= params
?.randomSerialNumberUpperCase
?? true;
260 params
.randomSerialNumber
= params
?.randomSerialNumber
?? true;
261 const serialNumberSuffix
= params
?.randomSerialNumber
262 ? ChargingStationUtils
.getRandomSerialNumberSuffix({
263 upperCase
: params
.randomSerialNumberUpperCase
,
266 stationInfo
.chargePointSerialNumber
=
267 stationTemplate
?.chargePointSerialNumberPrefix
&&
268 stationTemplate
.chargePointSerialNumberPrefix
+ serialNumberSuffix
;
269 stationInfo
.chargeBoxSerialNumber
=
270 stationTemplate
?.chargeBoxSerialNumberPrefix
&&
271 stationTemplate
.chargeBoxSerialNumberPrefix
+ serialNumberSuffix
;
272 stationInfo
.meterSerialNumber
=
273 stationTemplate
?.meterSerialNumberPrefix
&&
274 stationTemplate
.meterSerialNumberPrefix
+ serialNumberSuffix
;
277 public static propagateSerialNumber(
278 stationTemplate
: ChargingStationTemplate
,
279 stationInfoSrc
: ChargingStationInfo
,
280 stationInfoDst
: ChargingStationInfo
= {} as ChargingStationInfo
282 if (!stationInfoSrc
|| !stationTemplate
) {
284 'Missing charging station template or existing configuration to propagate serial number'
287 stationTemplate
?.chargePointSerialNumberPrefix
&& stationInfoSrc
?.chargePointSerialNumber
288 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
289 : stationInfoDst
?.chargePointSerialNumber
&& delete stationInfoDst
.chargePointSerialNumber
;
290 stationTemplate
?.chargeBoxSerialNumberPrefix
&& stationInfoSrc
?.chargeBoxSerialNumber
291 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
292 : stationInfoDst
?.chargeBoxSerialNumber
&& delete stationInfoDst
.chargeBoxSerialNumber
;
293 stationTemplate
?.meterSerialNumberPrefix
&& stationInfoSrc
?.meterSerialNumber
294 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
295 : stationInfoDst
?.meterSerialNumber
&& delete stationInfoDst
.meterSerialNumber
;
298 public static getAmperageLimitationUnitDivider(stationInfo
: ChargingStationInfo
): number {
300 switch (stationInfo
.amperageLimitationUnit
) {
301 case AmpereUnits
.DECI_AMPERE
:
304 case AmpereUnits
.CENTI_AMPERE
:
307 case AmpereUnits
.MILLI_AMPERE
:
315 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
317 * @param {ChargingProfile[]} chargingProfiles
318 * @param {string} logPrefix
319 * @returns {{ limit, matchingChargingProfile }}
321 public static getLimitFromChargingProfiles(
322 chargingProfiles
: ChargingProfile
[],
326 matchingChargingProfile
: ChargingProfile
;
328 for (const chargingProfile
of chargingProfiles
) {
330 const currentMoment
= moment();
331 const chargingSchedule
= chargingProfile
.chargingSchedule
;
332 // Check type (recurring) and if it is already active
333 // Adjust the daily recurring schedule to today
335 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
336 chargingProfile
.recurrencyKind
=== RecurrencyKindType
.DAILY
&&
337 currentMoment
.isAfter(chargingSchedule
.startSchedule
)
339 const currentDate
= new Date();
340 chargingSchedule
.startSchedule
= new Date(chargingSchedule
.startSchedule
);
341 chargingSchedule
.startSchedule
.setFullYear(
342 currentDate
.getFullYear(),
343 currentDate
.getMonth(),
344 currentDate
.getDate()
346 // Check if the start of the schedule is yesterday
347 if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
348 chargingSchedule
.startSchedule
.setDate(currentDate
.getDate() - 1);
350 } else if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
353 // Check if the charging profile is active
355 moment(chargingSchedule
.startSchedule
)
356 .add(chargingSchedule
.duration
, 's')
357 .isAfter(currentMoment
)
359 let lastButOneSchedule
: ChargingSchedulePeriod
;
360 // Search the right schedule period
361 for (const schedulePeriod
of chargingSchedule
.chargingSchedulePeriod
) {
362 // Handling of only one period
364 chargingSchedule
.chargingSchedulePeriod
.length
=== 1 &&
365 schedulePeriod
.startPeriod
=== 0
368 limit
: schedulePeriod
.limit
,
369 matchingChargingProfile
: chargingProfile
,
372 `${logPrefix} Matching charging profile found for power limitation: %j`,
377 // Find the right schedule period
379 moment(chargingSchedule
.startSchedule
)
380 .add(schedulePeriod
.startPeriod
, 's')
381 .isAfter(currentMoment
)
383 // Found the schedule: last but one is the correct one
385 limit
: lastButOneSchedule
.limit
,
386 matchingChargingProfile
: chargingProfile
,
389 `${logPrefix} Matching charging profile found for power limitation: %j`,
395 lastButOneSchedule
= schedulePeriod
;
396 // Handle the last schedule period
398 schedulePeriod
.startPeriod
===
399 chargingSchedule
.chargingSchedulePeriod
[
400 chargingSchedule
.chargingSchedulePeriod
.length
- 1
404 limit
: lastButOneSchedule
.limit
,
405 matchingChargingProfile
: chargingProfile
,
408 `${logPrefix} Matching charging profile found for power limitation: %j`,
419 public static getDefaultVoltageOut(
420 currentType
: CurrentType
,
421 templateFile
: string,
424 const errMsg
= `${logPrefix} Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
425 let defaultVoltageOut
: number;
426 switch (currentType
) {
428 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
431 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
434 logger
.error(errMsg
);
435 throw new Error(errMsg
);
437 return defaultVoltageOut
;
440 public static getSampledValueTemplate(
441 chargingStation
: ChargingStation
,
443 measurand
: MeterValueMeasurand
= MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
444 phase
?: MeterValuePhase
445 ): SampledValueTemplate
| undefined {
446 const onPhaseStr
= phase
? `on phase ${phase} ` : '';
447 if (!Constants
.SUPPORTED_MEASURANDS
.includes(measurand
)) {
449 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
454 measurand
!== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
455 !ChargingStationConfigurationUtils
.getConfigurationKey(
457 StandardParametersKey
.MeterValuesSampledData
458 )?.value
.includes(measurand
)
461 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId} not found in '${
462 StandardParametersKey.MeterValuesSampledData
467 const sampledValueTemplates
: SampledValueTemplate
[] =
468 chargingStation
.getConnectorStatus(connectorId
).MeterValues
;
471 !Utils
.isEmptyArray(sampledValueTemplates
) && index
< sampledValueTemplates
.length
;
475 !Constants
.SUPPORTED_MEASURANDS
.includes(
476 sampledValueTemplates
[index
]?.measurand
??
477 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
481 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
485 sampledValueTemplates
[index
]?.phase
=== phase
&&
486 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
487 ChargingStationConfigurationUtils
.getConfigurationKey(
489 StandardParametersKey
.MeterValuesSampledData
490 )?.value
.includes(measurand
)
492 return sampledValueTemplates
[index
];
495 !sampledValueTemplates
[index
].phase
&&
496 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
497 ChargingStationConfigurationUtils
.getConfigurationKey(
499 StandardParametersKey
.MeterValuesSampledData
500 )?.value
.includes(measurand
)
502 return sampledValueTemplates
[index
];
504 measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
505 (!sampledValueTemplates
[index
].measurand
||
506 sampledValueTemplates
[index
].measurand
=== measurand
)
508 return sampledValueTemplates
[index
];
511 if (measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
) {
512 const errorMsg
= `${chargingStation.logPrefix()} Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`;
513 logger
.error(errorMsg
);
514 throw new Error(errorMsg
);
517 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
521 public static getAuthorizationFile(stationInfo
: ChargingStationInfo
): string | undefined {
523 stationInfo
.authorizationFile
&&
525 path
.resolve(path
.dirname(fileURLToPath(import.meta
.url
)), '../'),
527 path
.basename(stationInfo
.authorizationFile
)
532 private static getRandomSerialNumberSuffix(params
?: {
533 randomBytesLength
?: number;
536 const randomSerialNumberSuffix
= crypto
537 .randomBytes(params
?.randomBytesLength
?? 16)
539 if (params
?.upperCase
) {
540 return randomSerialNumberSuffix
.toUpperCase();
542 return randomSerialNumberSuffix
;