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 { FileType
} from
'../types/FileType';
18 import FileUtils from
'../utils/FileUtils';
19 import { SampledValueTemplate
} from
'../types/MeasurandPerPhaseSampledValueTemplates';
20 import { StandardParametersKey
} from
'../types/ocpp/Configuration';
21 import Utils from
'../utils/Utils';
22 import { WebSocketCloseEventStatusString
} from
'../types/WebSocket';
23 import { WorkerProcessType
} from
'../types/Worker';
24 import crypto from
'crypto';
26 import logger from
'../utils/Logger';
27 import moment from
'moment';
28 import path from
'path';
30 export class ChargingStationUtils
{
31 public static getChargingStationId(
33 stationTemplate
: ChargingStationTemplate
35 // In case of multiple instances: add instance index to charging station id
36 const instanceIndex
= process
.env
.CF_INSTANCE_INDEX
?? 0;
37 const idSuffix
= stationTemplate
.nameSuffix
?? '';
38 const idStr
= '000000000' + index
.toString();
39 return stationTemplate
?.fixedName
40 ? stationTemplate
.baseName
41 : stationTemplate
.baseName
+
43 instanceIndex
.toString() +
44 idStr
.substring(idStr
.length
- 4) +
48 public static getHashId(index
: number, stationTemplate
: ChargingStationTemplate
): string {
49 const hashBootNotificationRequest
= {
50 chargePointModel
: stationTemplate
.chargePointModel
,
51 chargePointVendor
: stationTemplate
.chargePointVendor
,
52 ...(!Utils
.isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
53 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
55 ...(!Utils
.isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
56 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
58 ...(!Utils
.isUndefined(stationTemplate
.firmwareVersion
) && {
59 firmwareVersion
: stationTemplate
.firmwareVersion
,
61 ...(!Utils
.isUndefined(stationTemplate
.iccid
) && { iccid
: stationTemplate
.iccid
}),
62 ...(!Utils
.isUndefined(stationTemplate
.imsi
) && { imsi
: stationTemplate
.imsi
}),
63 ...(!Utils
.isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
64 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
66 ...(!Utils
.isUndefined(stationTemplate
.meterType
) && {
67 meterType
: stationTemplate
.meterType
,
71 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
73 JSON
.stringify(hashBootNotificationRequest
) +
74 ChargingStationUtils
.getChargingStationId(index
, stationTemplate
)
79 public static getTemplateMaxNumberOfConnectors(stationTemplate
: ChargingStationTemplate
): number {
80 const templateConnectors
= stationTemplate
?.Connectors
;
81 if (!templateConnectors
) {
84 return Object.keys(templateConnectors
).length
;
87 public static checkTemplateMaxConnectors(
88 templateMaxConnectors
: number,
92 if (templateMaxConnectors
=== 0) {
94 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
96 } else if (templateMaxConnectors
< 0) {
98 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
103 public static getConfiguredNumberOfConnectors(
105 stationTemplate
: ChargingStationTemplate
107 let configuredMaxConnectors
: number;
108 if (!Utils
.isEmptyArray(stationTemplate
.numberOfConnectors
)) {
109 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[];
110 // Distribute evenly the number of connectors
111 configuredMaxConnectors
= numberOfConnectors
[(index
- 1) % numberOfConnectors
.length
];
112 } else if (!Utils
.isUndefined(stationTemplate
.numberOfConnectors
)) {
113 configuredMaxConnectors
= stationTemplate
.numberOfConnectors
as number;
115 configuredMaxConnectors
= stationTemplate
?.Connectors
[0]
116 ? ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
) - 1
117 : ChargingStationUtils
.getTemplateMaxNumberOfConnectors(stationTemplate
);
119 return configuredMaxConnectors
;
122 public static checkConfiguredMaxConnectors(
123 configuredMaxConnectors
: number,
124 templateFile
: string,
127 if (configuredMaxConnectors
<= 0) {
129 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
134 public static createBootNotificationRequest(
135 stationInfo
: ChargingStationInfo
136 ): BootNotificationRequest
{
138 chargePointModel
: stationInfo
.chargePointModel
,
139 chargePointVendor
: stationInfo
.chargePointVendor
,
140 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
141 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
143 ...(!Utils
.isUndefined(stationInfo
.chargePointSerialNumber
) && {
144 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
146 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
147 firmwareVersion
: stationInfo
.firmwareVersion
,
149 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
150 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
151 ...(!Utils
.isUndefined(stationInfo
.meterSerialNumber
) && {
152 meterSerialNumber
: stationInfo
.meterSerialNumber
,
154 ...(!Utils
.isUndefined(stationInfo
.meterType
) && {
155 meterType
: stationInfo
.meterType
,
160 public static workerPoolInUse(): boolean {
161 return [WorkerProcessType
.DYNAMIC_POOL
, WorkerProcessType
.STATIC_POOL
].includes(
162 Configuration
.getWorkerProcess()
166 public static workerDynamicPoolInUse(): boolean {
167 return Configuration
.getWorkerProcess() === WorkerProcessType
.DYNAMIC_POOL
;
171 * Convert websocket error code to human readable string message
173 * @param code websocket error code
174 * @returns human readable string message
176 public static getWebSocketCloseEventStatusString(code
: number): string {
177 if (code
>= 0 && code
<= 999) {
179 } else if (code
>= 1016) {
181 return '(For WebSocket standard)';
182 } else if (code
<= 2999) {
183 return '(For WebSocket extensions)';
184 } else if (code
<= 3999) {
185 return '(For libraries and frameworks)';
186 } else if (code
<= 4999) {
187 return '(For applications)';
190 if (!Utils
.isUndefined(WebSocketCloseEventStatusString
[code
])) {
191 return WebSocketCloseEventStatusString
[code
] as string;
196 public static warnDeprecatedTemplateKey(
197 template
: ChargingStationTemplate
,
199 templateFile
: string,
203 if (!Utils
.isUndefined(template
[key
])) {
205 `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
206 logMsgToAppend && '. ' + logMsgToAppend
212 public static convertDeprecatedTemplateKey(
213 template
: ChargingStationTemplate
,
214 deprecatedKey
: string,
217 if (!Utils
.isUndefined(template
[deprecatedKey
])) {
218 template
[key
] = template
[deprecatedKey
] as unknown
;
219 delete template
[deprecatedKey
];
223 public static stationTemplateToStationInfo(
224 stationTemplate
: ChargingStationTemplate
225 ): ChargingStationInfo
{
226 stationTemplate
= Utils
.cloneObject(stationTemplate
);
227 delete stationTemplate
.power
;
228 delete stationTemplate
.powerUnit
;
229 delete stationTemplate
.Configuration
;
230 delete stationTemplate
.AutomaticTransactionGenerator
;
231 delete stationTemplate
.chargeBoxSerialNumberPrefix
;
232 delete stationTemplate
.chargePointSerialNumberPrefix
;
233 delete stationTemplate
.meterSerialNumberPrefix
;
234 return stationTemplate
;
237 public static createStationInfoHash(stationInfo
: ChargingStationInfo
): void {
238 const previousInfoHash
= stationInfo
?.infoHash
?? '';
239 delete stationInfo
.infoHash
;
240 const currentInfoHash
= crypto
241 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
242 .update(JSON
.stringify(stationInfo
))
245 Utils
.isEmptyString(previousInfoHash
) ||
246 (!Utils
.isEmptyString(previousInfoHash
) && currentInfoHash
!== previousInfoHash
)
248 stationInfo
.infoHash
= currentInfoHash
;
250 stationInfo
.infoHash
= previousInfoHash
;
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 for (const chargingProfile
of chargingProfiles
) {
337 const currentMoment
= moment();
338 const chargingSchedule
= chargingProfile
.chargingSchedule
;
339 // Check type (recurring) and if it is already active
340 // Adjust the daily recurring schedule to today
342 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
343 chargingProfile
.recurrencyKind
=== RecurrencyKindType
.DAILY
&&
344 currentMoment
.isAfter(chargingSchedule
.startSchedule
)
346 const currentDate
= new Date();
347 chargingSchedule
.startSchedule
= new Date(chargingSchedule
.startSchedule
);
348 chargingSchedule
.startSchedule
.setFullYear(
349 currentDate
.getFullYear(),
350 currentDate
.getMonth(),
351 currentDate
.getDate()
353 // Check if the start of the schedule is yesterday
354 if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
355 chargingSchedule
.startSchedule
.setDate(currentDate
.getDate() - 1);
357 } else if (moment(chargingSchedule
.startSchedule
).isAfter(currentMoment
)) {
360 // Check if the charging profile is active
362 moment(chargingSchedule
.startSchedule
)
363 .add(chargingSchedule
.duration
, 's')
364 .isAfter(currentMoment
)
366 let lastButOneSchedule
: ChargingSchedulePeriod
;
367 // Search the right schedule period
368 for (const schedulePeriod
of chargingSchedule
.chargingSchedulePeriod
) {
369 // Handling of only one period
371 chargingSchedule
.chargingSchedulePeriod
.length
=== 1 &&
372 schedulePeriod
.startPeriod
=== 0
375 limit
: schedulePeriod
.limit
,
376 matchingChargingProfile
: chargingProfile
,
379 `${logPrefix} Matching charging profile found for power limitation: %j`,
384 // Find the right schedule period
386 moment(chargingSchedule
.startSchedule
)
387 .add(schedulePeriod
.startPeriod
, 's')
388 .isAfter(currentMoment
)
390 // Found the schedule: last but one is the correct one
392 limit
: lastButOneSchedule
.limit
,
393 matchingChargingProfile
: chargingProfile
,
396 `${logPrefix} Matching charging profile found for power limitation: %j`,
402 lastButOneSchedule
= schedulePeriod
;
403 // Handle the last schedule period
405 schedulePeriod
.startPeriod
===
406 chargingSchedule
.chargingSchedulePeriod
[
407 chargingSchedule
.chargingSchedulePeriod
.length
- 1
411 limit
: lastButOneSchedule
.limit
,
412 matchingChargingProfile
: chargingProfile
,
415 `${logPrefix} Matching charging profile found for power limitation: %j`,
426 public static getDefaultVoltageOut(
427 currentType
: CurrentType
,
428 templateFile
: string,
431 const errMsg
= `${logPrefix} Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
432 let defaultVoltageOut
: number;
433 switch (currentType
) {
435 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
438 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
441 logger
.error(errMsg
);
442 throw new Error(errMsg
);
444 return defaultVoltageOut
;
447 public static getSampledValueTemplate(
448 chargingStation
: ChargingStation
,
450 measurand
: MeterValueMeasurand
= MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
451 phase
?: MeterValuePhase
452 ): SampledValueTemplate
| undefined {
453 const onPhaseStr
= phase
? `on phase ${phase} ` : '';
454 if (!Constants
.SUPPORTED_MEASURANDS
.includes(measurand
)) {
456 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
461 measurand
!== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
462 !ChargingStationConfigurationUtils
.getConfigurationKey(
464 StandardParametersKey
.MeterValuesSampledData
465 )?.value
.includes(measurand
)
468 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId} not found in '${
469 StandardParametersKey.MeterValuesSampledData
474 const sampledValueTemplates
: SampledValueTemplate
[] =
475 chargingStation
.getConnectorStatus(connectorId
).MeterValues
;
478 !Utils
.isEmptyArray(sampledValueTemplates
) && index
< sampledValueTemplates
.length
;
482 !Constants
.SUPPORTED_MEASURANDS
.includes(
483 sampledValueTemplates
[index
]?.measurand
??
484 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
488 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
492 sampledValueTemplates
[index
]?.phase
=== phase
&&
493 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
494 ChargingStationConfigurationUtils
.getConfigurationKey(
496 StandardParametersKey
.MeterValuesSampledData
497 )?.value
.includes(measurand
)
499 return sampledValueTemplates
[index
];
502 !sampledValueTemplates
[index
].phase
&&
503 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
504 ChargingStationConfigurationUtils
.getConfigurationKey(
506 StandardParametersKey
.MeterValuesSampledData
507 )?.value
.includes(measurand
)
509 return sampledValueTemplates
[index
];
511 measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
512 (!sampledValueTemplates
[index
].measurand
||
513 sampledValueTemplates
[index
].measurand
=== measurand
)
515 return sampledValueTemplates
[index
];
518 if (measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
) {
519 const errorMsg
= `${chargingStation.logPrefix()} Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`;
520 logger
.error(errorMsg
);
521 throw new Error(errorMsg
);
524 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
528 public static getAuthorizedTags(
529 stationInfo
: ChargingStationInfo
,
530 templateFile
: string,
533 let authorizedTags
: string[] = [];
534 const authorizationFile
= ChargingStationUtils
.getAuthorizationFile(stationInfo
);
535 if (authorizationFile
) {
537 // Load authorization file
538 authorizedTags
= JSON
.parse(fs
.readFileSync(authorizationFile
, 'utf8')) as string[];
540 FileUtils
.handleFileException(
542 FileType
.Authorization
,
544 error
as NodeJS
.ErrnoException
548 logger
.info(logPrefix
+ ' No authorization file given in template file ' + templateFile
);
550 return authorizedTags
;
553 public static getAuthorizationFile(stationInfo
: ChargingStationInfo
): string | undefined {
555 stationInfo
.authorizationFile
&&
557 path
.resolve(__dirname
, '../'),
559 path
.basename(stationInfo
.authorizationFile
)
564 private static getRandomSerialNumberSuffix(params
?: {
565 randomBytesLength
?: number;
568 const randomSerialNumberSuffix
= crypto
569 .randomBytes(params
?.randomBytesLength
?? 16)
571 if (params
?.upperCase
) {
572 return randomSerialNumberSuffix
.toUpperCase();
574 return randomSerialNumberSuffix
;