Ensure start transaction payload is always compliant with OCA specs
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStationUtils.ts
CommitLineData
8114d10e
JB
1import crypto from 'crypto';
2import path from 'path';
3import { fileURLToPath } from 'url';
4
5import moment from 'moment';
6
7import BaseError from '../exception/BaseError';
981ebfbe
JB
8import type { ChargingStationInfo } from '../types/ChargingStationInfo';
9import {
492cf6ab 10 AmpereUnits,
981ebfbe 11 type ChargingStationTemplate,
492cf6ab
JB
12 CurrentType,
13 Voltage,
14} from '../types/ChargingStationTemplate';
8114d10e 15import { ChargingProfileKindType, RecurrencyKindType } from '../types/ocpp/1.6/ChargingProfile';
d270cc87
JB
16import type { OCPP16BootNotificationRequest } from '../types/ocpp/1.6/Requests';
17import { BootReasonEnumType, OCPP20BootNotificationRequest } from '../types/ocpp/2.0/Requests';
6c1761d4 18import type { ChargingProfile, ChargingSchedulePeriod } from '../types/ocpp/ChargingProfile';
d270cc87 19import { OCPPVersion } from '../types/ocpp/OCPPVersion';
ed3d2808 20import type { BootNotificationRequest } from '../types/ocpp/Requests';
17ac262c 21import { WorkerProcessType } from '../types/Worker';
8114d10e
JB
22import Configuration from '../utils/Configuration';
23import Constants from '../utils/Constants';
17ac262c 24import logger from '../utils/Logger';
8114d10e 25import Utils from '../utils/Utils';
17ac262c 26
91a4f151
JB
27const moduleName = 'ChargingStationUtils';
28
17ac262c 29export class ChargingStationUtils {
d5bd1c00
JB
30 private constructor() {
31 // This is intentional
32 }
33
17ac262c
JB
34 public static getChargingStationId(
35 index: number,
36 stationTemplate: ChargingStationTemplate
37 ): string {
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();
ccb1d6e9 42 return stationTemplate?.fixedName
17ac262c
JB
43 ? stationTemplate.baseName
44 : stationTemplate.baseName +
45 '-' +
46 instanceIndex.toString() +
47 idStr.substring(idStr.length - 4) +
48 idSuffix;
49 }
50
fa7bccf4 51 public static getHashId(index: number, stationTemplate: ChargingStationTemplate): string {
99e92237 52 const chargingStationInfo = {
fa7bccf4
JB
53 chargePointModel: stationTemplate.chargePointModel,
54 chargePointVendor: stationTemplate.chargePointVendor,
55 ...(!Utils.isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && {
56 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix,
17ac262c 57 }),
fa7bccf4
JB
58 ...(!Utils.isUndefined(stationTemplate.chargePointSerialNumberPrefix) && {
59 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix,
17ac262c 60 }),
33d7ecc7 61 // FIXME?: Should a firmware version change always reference a new configuration file?
fa7bccf4
JB
62 ...(!Utils.isUndefined(stationTemplate.firmwareVersion) && {
63 firmwareVersion: stationTemplate.firmwareVersion,
17ac262c 64 }),
fa7bccf4
JB
65 ...(!Utils.isUndefined(stationTemplate.iccid) && { iccid: stationTemplate.iccid }),
66 ...(!Utils.isUndefined(stationTemplate.imsi) && { imsi: stationTemplate.imsi }),
67 ...(!Utils.isUndefined(stationTemplate.meterSerialNumberPrefix) && {
68 meterSerialNumber: stationTemplate.meterSerialNumberPrefix,
17ac262c 69 }),
fa7bccf4
JB
70 ...(!Utils.isUndefined(stationTemplate.meterType) && {
71 meterType: stationTemplate.meterType,
17ac262c
JB
72 }),
73 };
74 return crypto
75 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
fa7bccf4 76 .update(
99e92237 77 JSON.stringify(chargingStationInfo) +
fa7bccf4
JB
78 ChargingStationUtils.getChargingStationId(index, stationTemplate)
79 )
17ac262c
JB
80 .digest('hex');
81 }
82
fa7bccf4
JB
83 public static getTemplateMaxNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
84 const templateConnectors = stationTemplate?.Connectors;
85 if (!templateConnectors) {
86 return -1;
87 }
88 return Object.keys(templateConnectors).length;
89 }
90
91 public static checkTemplateMaxConnectors(
92 templateMaxConnectors: number,
93 templateFile: string,
94 logPrefix: string
95 ): void {
96 if (templateMaxConnectors === 0) {
97 logger.warn(
98 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
99 );
100 } else if (templateMaxConnectors < 0) {
101 logger.error(
102 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
103 );
104 }
105 }
106
c72f6634 107 public static getConfiguredNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
fa7bccf4 108 let configuredMaxConnectors: number;
c72f6634 109 if (Utils.isEmptyArray(stationTemplate.numberOfConnectors) === false) {
fa7bccf4 110 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
c72f6634
JB
111 configuredMaxConnectors =
112 numberOfConnectors[Math.floor(Utils.secureRandom() * numberOfConnectors.length)];
113 } else if (Utils.isUndefined(stationTemplate.numberOfConnectors) === false) {
fa7bccf4
JB
114 configuredMaxConnectors = stationTemplate.numberOfConnectors as number;
115 } else {
116 configuredMaxConnectors = stationTemplate?.Connectors[0]
117 ? ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate) - 1
118 : ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate);
119 }
120 return configuredMaxConnectors;
121 }
122
123 public static checkConfiguredMaxConnectors(
124 configuredMaxConnectors: number,
125 templateFile: string,
126 logPrefix: string
127 ): void {
128 if (configuredMaxConnectors <= 0) {
129 logger.warn(
130 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
131 );
132 }
133 }
134
17ac262c
JB
135 public static createBootNotificationRequest(
136 stationInfo: ChargingStationInfo
137 ): BootNotificationRequest {
d270cc87
JB
138 const ocppVersion = stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
139 switch (ocppVersion) {
140 case OCPPVersion.VERSION_16:
141 return {
142 chargePointModel: stationInfo.chargePointModel,
143 chargePointVendor: stationInfo.chargePointVendor,
144 ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && {
145 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber,
146 }),
147 ...(!Utils.isUndefined(stationInfo.chargePointSerialNumber) && {
148 chargePointSerialNumber: stationInfo.chargePointSerialNumber,
149 }),
150 ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
151 firmwareVersion: stationInfo.firmwareVersion,
152 }),
153 ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
154 ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
155 ...(!Utils.isUndefined(stationInfo.meterSerialNumber) && {
156 meterSerialNumber: stationInfo.meterSerialNumber,
157 }),
158 ...(!Utils.isUndefined(stationInfo.meterType) && {
159 meterType: stationInfo.meterType,
160 }),
161 } as OCPP16BootNotificationRequest;
162 case OCPPVersion.VERSION_20:
163 case OCPPVersion.VERSION_201:
164 return {
165 reason: BootReasonEnumType.PowerUp,
166 chargingStation: {
167 model: stationInfo.chargePointModel,
168 vendorName: stationInfo.chargePointVendor,
169 ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
170 firmwareVersion: stationInfo.firmwareVersion,
171 }),
172 ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && {
173 serialNumber: stationInfo.chargeBoxSerialNumber,
174 }),
98fc1389
JB
175 ...((!Utils.isUndefined(stationInfo.iccid) || !Utils.isUndefined(stationInfo.imsi)) && {
176 modem: {
177 ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
178 ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
179 },
180 }),
d270cc87
JB
181 },
182 } as OCPP20BootNotificationRequest;
183 }
17ac262c
JB
184 }
185
186 public static workerPoolInUse(): boolean {
187 return [WorkerProcessType.DYNAMIC_POOL, WorkerProcessType.STATIC_POOL].includes(
cf2a5d9b 188 Configuration.getWorker().processType
17ac262c
JB
189 );
190 }
191
192 public static workerDynamicPoolInUse(): boolean {
cf2a5d9b 193 return Configuration.getWorker().processType === WorkerProcessType.DYNAMIC_POOL;
17ac262c
JB
194 }
195
17ac262c
JB
196 public static warnDeprecatedTemplateKey(
197 template: ChargingStationTemplate,
198 key: string,
199 templateFile: string,
200 logPrefix: string,
201 logMsgToAppend = ''
202 ): void {
203 if (!Utils.isUndefined(template[key])) {
17ac262c
JB
204 logger.warn(
205 `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
206 logMsgToAppend && '. ' + logMsgToAppend
207 }`
208 );
209 }
210 }
211
212 public static convertDeprecatedTemplateKey(
213 template: ChargingStationTemplate,
214 deprecatedKey: string,
215 key: string
216 ): void {
217 if (!Utils.isUndefined(template[deprecatedKey])) {
218 template[key] = template[deprecatedKey] as unknown;
219 delete template[deprecatedKey];
220 }
221 }
222
fa7bccf4
JB
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;
fec4d204 233 delete stationTemplate.meterSerialNumberPrefix;
51c83d6f 234 return stationTemplate as unknown as ChargingStationInfo;
fa7bccf4
JB
235 }
236
237 public static createStationInfoHash(stationInfo: ChargingStationInfo): void {
ccb1d6e9 238 delete stationInfo.infoHash;
7c72977b 239 stationInfo.infoHash = crypto
ccb1d6e9
JB
240 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
241 .update(JSON.stringify(stationInfo))
242 .digest('hex');
17ac262c
JB
243 }
244
245 public static createSerialNumber(
fa7bccf4 246 stationTemplate: ChargingStationTemplate,
4d20f040 247 stationInfo: ChargingStationInfo,
fa7bccf4
JB
248 params: {
249 randomSerialNumberUpperCase?: boolean;
250 randomSerialNumber?: boolean;
251 } = {
17ac262c
JB
252 randomSerialNumberUpperCase: true,
253 randomSerialNumber: true,
254 }
255 ): void {
256 params = params ?? {};
257 params.randomSerialNumberUpperCase = params?.randomSerialNumberUpperCase ?? true;
258 params.randomSerialNumber = params?.randomSerialNumber ?? true;
fa7bccf4
JB
259 const serialNumberSuffix = params?.randomSerialNumber
260 ? ChargingStationUtils.getRandomSerialNumberSuffix({
261 upperCase: params.randomSerialNumberUpperCase,
262 })
263 : '';
fec4d204
JB
264 stationInfo.chargePointSerialNumber =
265 stationTemplate?.chargePointSerialNumberPrefix &&
266 stationTemplate.chargePointSerialNumberPrefix + serialNumberSuffix;
267 stationInfo.chargeBoxSerialNumber =
268 stationTemplate?.chargeBoxSerialNumberPrefix &&
269 stationTemplate.chargeBoxSerialNumberPrefix + serialNumberSuffix;
270 stationInfo.meterSerialNumber =
271 stationTemplate?.meterSerialNumberPrefix &&
272 stationTemplate.meterSerialNumberPrefix + serialNumberSuffix;
273 }
274
275 public static propagateSerialNumber(
276 stationTemplate: ChargingStationTemplate,
277 stationInfoSrc: ChargingStationInfo,
4d20f040 278 stationInfoDst: ChargingStationInfo
fec4d204
JB
279 ) {
280 if (!stationInfoSrc || !stationTemplate) {
baf93dda
JB
281 throw new BaseError(
282 'Missing charging station template or existing configuration to propagate serial number'
283 );
fec4d204
JB
284 }
285 stationTemplate?.chargePointSerialNumberPrefix && stationInfoSrc?.chargePointSerialNumber
286 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
287 : stationInfoDst?.chargePointSerialNumber && delete stationInfoDst.chargePointSerialNumber;
288 stationTemplate?.chargeBoxSerialNumberPrefix && stationInfoSrc?.chargeBoxSerialNumber
289 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
290 : stationInfoDst?.chargeBoxSerialNumber && delete stationInfoDst.chargeBoxSerialNumber;
291 stationTemplate?.meterSerialNumberPrefix && stationInfoSrc?.meterSerialNumber
292 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
293 : stationInfoDst?.meterSerialNumber && delete stationInfoDst.meterSerialNumber;
17ac262c
JB
294 }
295
296 public static getAmperageLimitationUnitDivider(stationInfo: ChargingStationInfo): number {
297 let unitDivider = 1;
298 switch (stationInfo.amperageLimitationUnit) {
299 case AmpereUnits.DECI_AMPERE:
300 unitDivider = 10;
301 break;
302 case AmpereUnits.CENTI_AMPERE:
303 unitDivider = 100;
304 break;
305 case AmpereUnits.MILLI_AMPERE:
306 unitDivider = 1000;
307 break;
308 }
309 return unitDivider;
310 }
311
312 /**
313 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
314 *
0e4fa348
JB
315 * @param chargingProfiles -
316 * @param logPrefix -
317 * @returns
17ac262c
JB
318 */
319 public static getLimitFromChargingProfiles(
320 chargingProfiles: ChargingProfile[],
321 logPrefix: string
322 ): {
323 limit: number;
324 matchingChargingProfile: ChargingProfile;
325 } | null {
91a4f151 326 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
17ac262c
JB
327 for (const chargingProfile of chargingProfiles) {
328 // Set helpers
329 const currentMoment = moment();
330 const chargingSchedule = chargingProfile.chargingSchedule;
331 // Check type (recurring) and if it is already active
332 // Adjust the daily recurring schedule to today
333 if (
334 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
335 chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
336 currentMoment.isAfter(chargingSchedule.startSchedule)
337 ) {
338 const currentDate = new Date();
339 chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule);
340 chargingSchedule.startSchedule.setFullYear(
341 currentDate.getFullYear(),
342 currentDate.getMonth(),
343 currentDate.getDate()
344 );
345 // Check if the start of the schedule is yesterday
346 if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
347 chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1);
348 }
349 } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
350 return null;
351 }
352 // Check if the charging profile is active
353 if (
354 moment(chargingSchedule.startSchedule)
355 .add(chargingSchedule.duration, 's')
356 .isAfter(currentMoment)
357 ) {
358 let lastButOneSchedule: ChargingSchedulePeriod;
359 // Search the right schedule period
360 for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
361 // Handling of only one period
362 if (
363 chargingSchedule.chargingSchedulePeriod.length === 1 &&
364 schedulePeriod.startPeriod === 0
365 ) {
366 const result = {
367 limit: schedulePeriod.limit,
368 matchingChargingProfile: chargingProfile,
369 };
91a4f151 370 logger.debug(debugLogMsg, result);
17ac262c
JB
371 return result;
372 }
373 // Find the right schedule period
374 if (
375 moment(chargingSchedule.startSchedule)
376 .add(schedulePeriod.startPeriod, 's')
377 .isAfter(currentMoment)
378 ) {
379 // Found the schedule: last but one is the correct one
380 const result = {
381 limit: lastButOneSchedule.limit,
382 matchingChargingProfile: chargingProfile,
383 };
91a4f151 384 logger.debug(debugLogMsg, result);
17ac262c
JB
385 return result;
386 }
387 // Keep it
388 lastButOneSchedule = schedulePeriod;
389 // Handle the last schedule period
390 if (
391 schedulePeriod.startPeriod ===
392 chargingSchedule.chargingSchedulePeriod[
393 chargingSchedule.chargingSchedulePeriod.length - 1
394 ].startPeriod
395 ) {
396 const result = {
397 limit: lastButOneSchedule.limit,
398 matchingChargingProfile: chargingProfile,
399 };
91a4f151 400 logger.debug(debugLogMsg, result);
17ac262c
JB
401 return result;
402 }
403 }
404 }
405 }
406 return null;
407 }
408
492cf6ab
JB
409 public static getDefaultVoltageOut(
410 currentType: CurrentType,
411 templateFile: string,
412 logPrefix: string
413 ): Voltage {
fc040c43 414 const errMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
492cf6ab
JB
415 let defaultVoltageOut: number;
416 switch (currentType) {
417 case CurrentType.AC:
418 defaultVoltageOut = Voltage.VOLTAGE_230;
419 break;
420 case CurrentType.DC:
421 defaultVoltageOut = Voltage.VOLTAGE_400;
422 break;
423 default:
fc040c43 424 logger.error(`${logPrefix} ${errMsg}`);
6c8f5d90 425 throw new BaseError(errMsg);
492cf6ab
JB
426 }
427 return defaultVoltageOut;
428 }
429
fa7bccf4
JB
430 public static getAuthorizationFile(stationInfo: ChargingStationInfo): string | undefined {
431 return (
432 stationInfo.authorizationFile &&
433 path.join(
0d8140bd 434 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
fa7bccf4
JB
435 'assets',
436 path.basename(stationInfo.authorizationFile)
437 )
438 );
439 }
440
17ac262c
JB
441 private static getRandomSerialNumberSuffix(params?: {
442 randomBytesLength?: number;
443 upperCase?: boolean;
444 }): string {
445 const randomSerialNumberSuffix = crypto
446 .randomBytes(params?.randomBytesLength ?? 16)
447 .toString('hex');
448 if (params?.upperCase) {
449 return randomSerialNumberSuffix.toUpperCase();
450 }
451 return randomSerialNumberSuffix;
452 }
453}