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 { BootNotificationRequest
} from
'../types/ocpp/Requests';
11 import ChargingStation from
'./ChargingStation';
12 import { ChargingStationConfigurationUtils
} from
'./ChargingStationConfigurationUtils';
13 import ChargingStationInfo from
'../types/ChargingStationInfo';
14 import Configuration from
'../utils/Configuration';
15 import Constants from
'../utils/Constants';
16 import { FileType
} from
'../types/FileType';
17 import FileUtils from
'../utils/FileUtils';
18 import { SampledValueTemplate
} from
'../types/MeasurandPerPhaseSampledValueTemplates';
19 import { StandardParametersKey
} from
'../types/ocpp/Configuration';
20 import Utils from
'../utils/Utils';
21 import { WebSocketCloseEventStatusString
} from
'../types/WebSocket';
22 import { WorkerProcessType
} from
'../types/Worker';
23 import crypto from
'crypto';
25 import logger from
'../utils/Logger';
26 import moment from
'moment';
27 import path from
'path';
29 export class ChargingStationUtils
{
30 public static getChargingStationId(
32 stationTemplate
: ChargingStationTemplate
34 // In case of multiple instances: add instance index to charging station id
35 const instanceIndex
= process
.env
.CF_INSTANCE_INDEX
?? 0;
36 const idSuffix
= stationTemplate
.nameSuffix
?? '';
37 const idStr
= '000000000' + index
.toString();
38 return stationTemplate
?.fixedName
39 ? stationTemplate
.baseName
40 : stationTemplate
.baseName
+
42 instanceIndex
.toString() +
43 idStr
.substring(idStr
.length
- 4) +
47 public static getHashId(index
: number, stationTemplate
: ChargingStationTemplate
): string {
48 const hashBootNotificationRequest
= {
49 chargePointModel
: stationTemplate
.chargePointModel
,
50 chargePointVendor
: stationTemplate
.chargePointVendor
,
51 ...(!Utils
.isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
52 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
54 ...(!Utils
.isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
55 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
57 ...(!Utils
.isUndefined(stationTemplate
.firmwareVersion
) && {
58 firmwareVersion
: stationTemplate
.firmwareVersion
,
60 ...(!Utils
.isUndefined(stationTemplate
.iccid
) && { iccid
: stationTemplate
.iccid
}),
61 ...(!Utils
.isUndefined(stationTemplate
.imsi
) && { imsi
: stationTemplate
.imsi
}),
62 ...(!Utils
.isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
63 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
65 ...(!Utils
.isUndefined(stationTemplate
.meterType
) && {
66 meterType
: stationTemplate
.meterType
,
70 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
72 JSON
.stringify(hashBootNotificationRequest
) +
73 ChargingStationUtils
.getChargingStationId(index
, stationTemplate
)
78 public static getTemplateMaxNumberOfConnectors(stationTemplate
: ChargingStationTemplate
): number {
79 const templateConnectors
= stationTemplate
?.Connectors
;
80 if (!templateConnectors
) {
83 return Object.keys(templateConnectors
).length
;
86 public static checkTemplateMaxConnectors(
87 templateMaxConnectors
: number,
91 if (templateMaxConnectors
=== 0) {
93 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
95 } else if (templateMaxConnectors
< 0) {
97 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
102 public static getConfiguredNumberOfConnectors(
104 stationTemplate
: ChargingStationTemplate
106 let configuredMaxConnectors
: number;
107 if (!Utils
.isEmptyArray(stationTemplate
.numberOfConnectors
)) {
108 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
109 // Distribute evenly the number of connectors
110 configuredMaxConnectors
= numberOfConnectors
[(index
- 1) % numberOfConnectors
.length
];
111 } else if (!Utils
.isUndefined(stationTemplate
.numberOfConnectors
)) {
112 configuredMaxConnectors
= stationTemplate
.numberOfConnectors
as number;
114 configuredMaxConnectors
= stationTemplate
?.Connectors
[0]
115 ? ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
) - 1
116 : ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
);
118 return configuredMaxConnectors
;
121 public static checkConfiguredMaxConnectors(
122 configuredMaxConnectors
: number,
123 templateFile
: string,
126 if (configuredMaxConnectors
<= 0) {
128 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
133 public static createBootNotificationRequest(
134 stationInfo
: ChargingStationInfo
135 ): BootNotificationRequest
{
137 chargePointModel
: stationInfo
.chargePointModel
,
138 chargePointVendor
: stationInfo
.chargePointVendor
,
139 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
140 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
142 ...(!Utils
.isUndefined(stationInfo
.chargePointSerialNumber
) && {
143 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
145 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
146 firmwareVersion
: stationInfo
.firmwareVersion
,
148 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
149 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
150 ...(!Utils
.isUndefined(stationInfo
.meterSerialNumber
) && {
151 meterSerialNumber
: stationInfo
.meterSerialNumber
,
153 ...(!Utils
.isUndefined(stationInfo
.meterType
) && {
154 meterType
: stationInfo
.meterType
,
159 public static workerPoolInUse(): boolean {
160 return [WorkerProcessType
.DYNAMIC_POOL
, WorkerProcessType
.STATIC_POOL
].includes(
161 Configuration
.getWorkerProcess()
165 public static workerDynamicPoolInUse(): boolean {
166 return Configuration
.getWorkerProcess() === WorkerProcessType
.DYNAMIC_POOL
;
170 * Convert websocket error code to human readable string message
172 * @param code websocket error code
173 * @returns human readable string message
175 public static getWebSocketCloseEventStatusString(code
: number): string {
176 if (code
>= 0 && code
<= 999) {
178 } else if (code
>= 1016) {
180 return '(For WebSocket standard)';
181 } else if (code
<= 2999) {
182 return '(For WebSocket extensions)';
183 } else if (code
<= 3999) {
184 return '(For libraries and frameworks)';
185 } else if (code
<= 4999) {
186 return '(For applications)';
189 if (!Utils
.isUndefined(WebSocketCloseEventStatusString
[code
])) {
190 return WebSocketCloseEventStatusString
[code
] as string;
195 public static warnDeprecatedTemplateKey(
196 template
: ChargingStationTemplate
,
198 templateFile
: string,
202 if (!Utils
.isUndefined(template
[key
])) {
204 `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
205 logMsgToAppend && '. ' + logMsgToAppend
211 public static convertDeprecatedTemplateKey(
212 template
: ChargingStationTemplate
,
213 deprecatedKey
: string,
216 if (!Utils
.isUndefined(template
[deprecatedKey
])) {
217 template
[key
] = template
[deprecatedKey
] as unknown
;
218 delete template
[deprecatedKey
];
222 public static stationTemplateToStationInfo(
223 stationTemplate
: ChargingStationTemplate
224 ): ChargingStationInfo
{
225 stationTemplate
= Utils
.cloneObject(stationTemplate
);
226 delete stationTemplate
.power
;
227 delete stationTemplate
.powerUnit
;
228 delete stationTemplate
.Configuration
;
229 delete stationTemplate
.AutomaticTransactionGenerator
;
230 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
231 delete stationTemplate
.chargePointSerialNumberPrefix
;
232 return stationTemplate
;
235 public static createStationInfoHash(stationInfo
: ChargingStationInfo
): void {
236 const previousInfoHash
= stationInfo
?.infoHash
?? '';
237 delete stationInfo
.infoHash
;
238 const currentInfoHash
= crypto
239 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
240 .update(JSON
.stringify(stationInfo
))
243 Utils
.isEmptyString(previousInfoHash
) ||
244 (!Utils
.isEmptyString(previousInfoHash
) && currentInfoHash
!== previousInfoHash
)
246 stationInfo
.infoHash
= currentInfoHash
;
248 stationInfo
.infoHash
= previousInfoHash
;
252 public static createSerialNumber(
253 stationTemplate
: ChargingStationTemplate
,
254 stationInfo
: 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 stationTemplate
?.chargePointSerialNumberPrefix
&&
273 Utils
.isNullOrUndefined(stationInfo
?.chargePointSerialNumber
)
274 ? (stationInfo
.chargePointSerialNumber
=
275 stationTemplate
.chargePointSerialNumberPrefix
+ serialNumberSuffix
)
276 : stationInfo
&& delete stationInfo
.chargePointSerialNumber
;
277 stationTemplate
?.chargeBoxSerialNumberPrefix
&&
279 Utils
.isNullOrUndefined(stationInfo
?.chargeBoxSerialNumber
)
280 ? (stationInfo
.chargeBoxSerialNumber
=
281 stationTemplate
.chargeBoxSerialNumberPrefix
+ serialNumberSuffix
)
282 : stationInfo
&& delete stationInfo
.chargeBoxSerialNumber
;
283 stationTemplate
?.meterSerialNumberPrefix
&&
285 Utils
.isNullOrUndefined(stationInfo
?.meterSerialNumber
)
286 ? (stationInfo
.meterSerialNumber
=
287 stationTemplate
.meterSerialNumberPrefix
+ serialNumberSuffix
)
288 : stationInfo
&& delete stationInfo
.meterSerialNumber
;
291 public static getAmperageLimitationUnitDivider(stationInfo
: ChargingStationInfo
): number {
293 switch (stationInfo
.amperageLimitationUnit
) {
294 case AmpereUnits
.DECI_AMPERE
:
297 case AmpereUnits
.CENTI_AMPERE
:
300 case AmpereUnits
.MILLI_AMPERE
:
308 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
310 * @param {ChargingProfile[]} chargingProfiles
311 * @param {string} logPrefix
312 * @returns {{ limit, matchingChargingProfile }}
314 public static getLimitFromChargingProfiles(
315 chargingProfiles
: ChargingProfile
[],
319 matchingChargingProfile
: ChargingProfile
;
321 for (const chargingProfile
of chargingProfiles
) {
323 const currentMoment
= moment();
324 const chargingSchedule
= chargingProfile
.chargingSchedule
;
325 // Check type (recurring) and if it is already active
326 // Adjust the daily recurring schedule to today
328 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
329 chargingProfile
.recurrencyKind
=== RecurrencyKindType
.DAILY
&&
330 currentMoment
.isAfter(chargingSchedule
.startSchedule
)
332 const currentDate
= new Date();
333 chargingSchedule
.startSchedule
= new Date(chargingSchedule
.startSchedule
);
334 chargingSchedule
.startSchedule
.setFullYear(
335 currentDate
.getFullYear(),
336 currentDate
.getMonth(),
337 currentDate
.getDate()
339 // Check if the start of the schedule is yesterday
340 if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
341 chargingSchedule
.startSchedule
.setDate(currentDate
.getDate() - 1);
343 } else if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
346 // Check if the charging profile is active
348 moment(chargingSchedule
.startSchedule
)
349 .add(chargingSchedule
.duration
, 's')
350 .isAfter(currentMoment
)
352 let lastButOneSchedule
: ChargingSchedulePeriod
;
353 // Search the right schedule period
354 for (const schedulePeriod
of chargingSchedule
.chargingSchedulePeriod
) {
355 // Handling of only one period
357 chargingSchedule
.chargingSchedulePeriod
.length
=== 1 &&
358 schedulePeriod
.startPeriod
=== 0
361 limit
: schedulePeriod
.limit
,
362 matchingChargingProfile
: chargingProfile
,
365 `${logPrefix} Matching charging profile found for power limitation: %j`,
370 // Find the right schedule period
372 moment(chargingSchedule
.startSchedule
)
373 .add(schedulePeriod
.startPeriod
, 's')
374 .isAfter(currentMoment
)
376 // Found the schedule: last but one is the correct one
378 limit
: lastButOneSchedule
.limit
,
379 matchingChargingProfile
: chargingProfile
,
382 `${logPrefix} Matching charging profile found for power limitation: %j`,
388 lastButOneSchedule
= schedulePeriod
;
389 // Handle the last schedule period
391 schedulePeriod
.startPeriod
===
392 chargingSchedule
.chargingSchedulePeriod
[
393 chargingSchedule
.chargingSchedulePeriod
.length
- 1
397 limit
: lastButOneSchedule
.limit
,
398 matchingChargingProfile
: chargingProfile
,
401 `${logPrefix} Matching charging profile found for power limitation: %j`,
412 public static getDefaultVoltageOut(
413 currentType
: CurrentType
,
414 templateFile
: string,
417 const errMsg
= `${logPrefix} Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
418 let defaultVoltageOut
: number;
419 switch (currentType
) {
421 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
424 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
427 logger
.error(errMsg
);
428 throw new Error(errMsg
);
430 return defaultVoltageOut
;
433 public static getSampledValueTemplate(
434 chargingStation
: ChargingStation
,
436 measurand
: MeterValueMeasurand
= MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
437 phase
?: MeterValuePhase
438 ): SampledValueTemplate
| undefined {
439 const onPhaseStr
= phase
? `on phase ${phase} ` : '';
440 if (!Constants
.SUPPORTED_MEASURANDS
.includes(measurand
)) {
442 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
447 measurand
!== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
448 !ChargingStationConfigurationUtils
.getConfigurationKey(
450 StandardParametersKey
.MeterValuesSampledData
451 )?.value
.includes(measurand
)
454 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId} not found in '${
455 StandardParametersKey.MeterValuesSampledData
460 const sampledValueTemplates
: SampledValueTemplate
[] =
461 chargingStation
.getConnectorStatus(connectorId
).MeterValues
;
464 !Utils
.isEmptyArray(sampledValueTemplates
) && index
< sampledValueTemplates
.length
;
468 !Constants
.SUPPORTED_MEASURANDS
.includes(
469 sampledValueTemplates
[index
]?.measurand
??
470 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
474 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
478 sampledValueTemplates
[index
]?.phase
=== phase
&&
479 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
480 ChargingStationConfigurationUtils
.getConfigurationKey(
482 StandardParametersKey
.MeterValuesSampledData
483 )?.value
.includes(measurand
)
485 return sampledValueTemplates
[index
];
488 !sampledValueTemplates
[index
].phase
&&
489 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
490 ChargingStationConfigurationUtils
.getConfigurationKey(
492 StandardParametersKey
.MeterValuesSampledData
493 )?.value
.includes(measurand
)
495 return sampledValueTemplates
[index
];
497 measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
498 (!sampledValueTemplates
[index
].measurand
||
499 sampledValueTemplates
[index
].measurand
=== measurand
)
501 return sampledValueTemplates
[index
];
504 if (measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
) {
505 const errorMsg
= `${chargingStation.logPrefix()} Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`;
506 logger
.error(errorMsg
);
507 throw new Error(errorMsg
);
510 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
514 public static getAuthorizedTags(
515 stationInfo
: ChargingStationInfo
,
516 templateFile
: string,
519 let authorizedTags
: string[] = [];
520 const authorizationFile
= ChargingStationUtils
.getAuthorizationFile(stationInfo
);
521 if (authorizationFile
) {
523 // Load authorization file
524 authorizedTags
= JSON
.parse(fs
.readFileSync(authorizationFile
, 'utf8')) as string[];
526 FileUtils
.handleFileException(
528 FileType
.Authorization
,
530 error
as NodeJS
.ErrnoException
534 logger
.info(logPrefix
+ ' No authorization file given in template file ' + templateFile
);
536 return authorizedTags
;
539 public static getAuthorizationFile(stationInfo
: ChargingStationInfo
): string | undefined {
541 stationInfo
.authorizationFile
&&
543 path
.resolve(__dirname
, '../'),
545 path
.basename(stationInfo
.authorizationFile
)
550 private static getRandomSerialNumberSuffix(params
?: {
551 randomBytesLength
?: number;
554 const randomSerialNumberSuffix
= crypto
555 .randomBytes(params
?.randomBytesLength
?? 16)
557 if (params
?.upperCase
) {
558 return randomSerialNumberSuffix
.toUpperCase();
560 return randomSerialNumberSuffix
;