refactor: cleanup RFID tags authorization code
[e-mobility-charging-stations-simulator.git] / src / charging-station / Helpers.ts
1 import { createHash, randomBytes } from 'node:crypto';
2 import type { EventEmitter } from 'node:events';
3 import { basename, dirname, join } from 'node:path';
4 import { fileURLToPath } from 'node:url';
5
6 import chalk from 'chalk';
7 import {
8 addDays,
9 addSeconds,
10 addWeeks,
11 differenceInDays,
12 differenceInSeconds,
13 differenceInWeeks,
14 isAfter,
15 isBefore,
16 isDate,
17 isWithinInterval,
18 toDate,
19 } from 'date-fns';
20
21 import type { ChargingStation } from './ChargingStation';
22 import { getConfigurationKey } from './ConfigurationKeyUtils';
23 import { BaseError } from '../exception';
24 import {
25 AmpereUnits,
26 AvailabilityType,
27 type BootNotificationRequest,
28 BootReasonEnumType,
29 type ChargingProfile,
30 ChargingProfileKindType,
31 ChargingRateUnitType,
32 type ChargingSchedulePeriod,
33 type ChargingStationInfo,
34 type ChargingStationTemplate,
35 ChargingStationWorkerMessageEvents,
36 ConnectorPhaseRotation,
37 type ConnectorStatus,
38 ConnectorStatusEnum,
39 CurrentType,
40 type EvseTemplate,
41 type OCPP16BootNotificationRequest,
42 type OCPP20BootNotificationRequest,
43 OCPPVersion,
44 RecurrencyKindType,
45 StandardParametersKey,
46 SupportedFeatureProfiles,
47 Voltage,
48 } from '../types';
49 import {
50 ACElectricUtils,
51 Constants,
52 DCElectricUtils,
53 cloneObject,
54 convertToDate,
55 convertToInt,
56 isArraySorted,
57 isEmptyObject,
58 isEmptyString,
59 isNotEmptyArray,
60 isNotEmptyString,
61 isNullOrUndefined,
62 isUndefined,
63 isValidTime,
64 logger,
65 secureRandom,
66 } from '../utils';
67
68 const moduleName = 'Helpers';
69
70 export const getChargingStationId = (
71 index: number,
72 stationTemplate: ChargingStationTemplate,
73 ): string => {
74 // In case of multiple instances: add instance index to charging station id
75 const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0;
76 const idSuffix = stationTemplate?.nameSuffix ?? '';
77 const idStr = `000000000${index.toString()}`;
78 return stationTemplate?.fixedName
79 ? stationTemplate.baseName
80 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
81 idStr.length - 4,
82 )}${idSuffix}`;
83 };
84
85 export const countReservableConnectors = (connectors: Map<number, ConnectorStatus>) => {
86 let reservableConnectors = 0;
87 for (const [connectorId, connectorStatus] of connectors) {
88 if (connectorId === 0) {
89 continue;
90 }
91 if (connectorStatus.status === ConnectorStatusEnum.Available) {
92 ++reservableConnectors;
93 }
94 }
95 return reservableConnectors;
96 };
97
98 export const getHashId = (index: number, stationTemplate: ChargingStationTemplate): string => {
99 const chargingStationInfo = {
100 chargePointModel: stationTemplate.chargePointModel,
101 chargePointVendor: stationTemplate.chargePointVendor,
102 ...(!isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && {
103 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix,
104 }),
105 ...(!isUndefined(stationTemplate.chargePointSerialNumberPrefix) && {
106 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix,
107 }),
108 ...(!isUndefined(stationTemplate.meterSerialNumberPrefix) && {
109 meterSerialNumber: stationTemplate.meterSerialNumberPrefix,
110 }),
111 ...(!isUndefined(stationTemplate.meterType) && {
112 meterType: stationTemplate.meterType,
113 }),
114 };
115 return createHash(Constants.DEFAULT_HASH_ALGORITHM)
116 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
117 .digest('hex');
118 };
119
120 export const checkChargingStation = (
121 chargingStation: ChargingStation,
122 logPrefix: string,
123 ): boolean => {
124 if (chargingStation.started === false && chargingStation.starting === false) {
125 logger.warn(`${logPrefix} charging station is stopped, cannot proceed`);
126 return false;
127 }
128 return true;
129 };
130
131 export const getPhaseRotationValue = (
132 connectorId: number,
133 numberOfPhases: number,
134 ): string | undefined => {
135 // AC/DC
136 if (connectorId === 0 && numberOfPhases === 0) {
137 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
138 } else if (connectorId > 0 && numberOfPhases === 0) {
139 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
140 // AC
141 } else if (connectorId > 0 && numberOfPhases === 1) {
142 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
143 } else if (connectorId > 0 && numberOfPhases === 3) {
144 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
145 }
146 };
147
148 export const getMaxNumberOfEvses = (evses: Record<string, EvseTemplate>): number => {
149 if (!evses) {
150 return -1;
151 }
152 return Object.keys(evses).length;
153 };
154
155 const getMaxNumberOfConnectors = (connectors: Record<string, ConnectorStatus>): number => {
156 if (!connectors) {
157 return -1;
158 }
159 return Object.keys(connectors).length;
160 };
161
162 export const getBootConnectorStatus = (
163 chargingStation: ChargingStation,
164 connectorId: number,
165 connectorStatus: ConnectorStatus,
166 ): ConnectorStatusEnum => {
167 let connectorBootStatus: ConnectorStatusEnum;
168 if (
169 !connectorStatus?.status &&
170 (chargingStation.isChargingStationAvailable() === false ||
171 chargingStation.isConnectorAvailable(connectorId) === false)
172 ) {
173 connectorBootStatus = ConnectorStatusEnum.Unavailable;
174 } else if (!connectorStatus?.status && connectorStatus?.bootStatus) {
175 // Set boot status in template at startup
176 connectorBootStatus = connectorStatus?.bootStatus;
177 } else if (connectorStatus?.status) {
178 // Set previous status at startup
179 connectorBootStatus = connectorStatus?.status;
180 } else {
181 // Set default status
182 connectorBootStatus = ConnectorStatusEnum.Available;
183 }
184 return connectorBootStatus;
185 };
186
187 export const checkTemplate = (
188 stationTemplate: ChargingStationTemplate,
189 logPrefix: string,
190 templateFile: string,
191 ): void => {
192 if (isNullOrUndefined(stationTemplate)) {
193 const errorMsg = `Failed to read charging station template file ${templateFile}`;
194 logger.error(`${logPrefix} ${errorMsg}`);
195 throw new BaseError(errorMsg);
196 }
197 if (isEmptyObject(stationTemplate)) {
198 const errorMsg = `Empty charging station information from template file ${templateFile}`;
199 logger.error(`${logPrefix} ${errorMsg}`);
200 throw new BaseError(errorMsg);
201 }
202 if (isEmptyObject(stationTemplate.AutomaticTransactionGenerator!)) {
203 stationTemplate.AutomaticTransactionGenerator = Constants.DEFAULT_ATG_CONFIGURATION;
204 logger.warn(
205 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
206 Constants.DEFAULT_ATG_CONFIGURATION,
207 );
208 }
209 if (isNullOrUndefined(stationTemplate.idTagsFile) || isEmptyString(stationTemplate.idTagsFile)) {
210 logger.warn(
211 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`,
212 );
213 }
214 };
215
216 export const checkConnectorsConfiguration = (
217 stationTemplate: ChargingStationTemplate,
218 logPrefix: string,
219 templateFile: string,
220 ): {
221 configuredMaxConnectors: number;
222 templateMaxConnectors: number;
223 templateMaxAvailableConnectors: number;
224 } => {
225 const configuredMaxConnectors = getConfiguredNumberOfConnectors(stationTemplate);
226 checkConfiguredMaxConnectors(configuredMaxConnectors, logPrefix, templateFile);
227 const templateMaxConnectors = getMaxNumberOfConnectors(stationTemplate.Connectors!);
228 checkTemplateMaxConnectors(templateMaxConnectors, logPrefix, templateFile);
229 const templateMaxAvailableConnectors = stationTemplate.Connectors![0]
230 ? templateMaxConnectors - 1
231 : templateMaxConnectors;
232 if (
233 configuredMaxConnectors > templateMaxAvailableConnectors &&
234 !stationTemplate?.randomConnectors
235 ) {
236 logger.warn(
237 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`,
238 );
239 stationTemplate.randomConnectors = true;
240 }
241 return { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors };
242 };
243
244 export const checkStationInfoConnectorStatus = (
245 connectorId: number,
246 connectorStatus: ConnectorStatus,
247 logPrefix: string,
248 templateFile: string,
249 ): void => {
250 if (!isNullOrUndefined(connectorStatus?.status)) {
251 logger.warn(
252 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`,
253 );
254 delete connectorStatus.status;
255 }
256 };
257
258 export const buildConnectorsMap = (
259 connectors: Record<string, ConnectorStatus>,
260 logPrefix: string,
261 templateFile: string,
262 ): Map<number, ConnectorStatus> => {
263 const connectorsMap = new Map<number, ConnectorStatus>();
264 if (getMaxNumberOfConnectors(connectors) > 0) {
265 for (const connector in connectors) {
266 const connectorStatus = connectors[connector];
267 const connectorId = convertToInt(connector);
268 checkStationInfoConnectorStatus(connectorId, connectorStatus, logPrefix, templateFile);
269 connectorsMap.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus));
270 }
271 } else {
272 logger.warn(
273 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`,
274 );
275 }
276 return connectorsMap;
277 };
278
279 export const initializeConnectorsMapStatus = (
280 connectors: Map<number, ConnectorStatus>,
281 logPrefix: string,
282 ): void => {
283 for (const connectorId of connectors.keys()) {
284 if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
285 logger.warn(
286 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
287 connectorId,
288 )?.transactionId}`,
289 );
290 }
291 if (connectorId === 0) {
292 connectors.get(connectorId)!.availability = AvailabilityType.Operative;
293 if (isUndefined(connectors.get(connectorId)?.chargingProfiles)) {
294 connectors.get(connectorId)!.chargingProfiles = [];
295 }
296 } else if (
297 connectorId > 0 &&
298 isNullOrUndefined(connectors.get(connectorId)?.transactionStarted)
299 ) {
300 initializeConnectorStatus(connectors.get(connectorId)!);
301 }
302 }
303 };
304
305 export const resetConnectorStatus = (connectorStatus: ConnectorStatus): void => {
306 connectorStatus.idTagLocalAuthorized = false;
307 connectorStatus.idTagAuthorized = false;
308 connectorStatus.transactionRemoteStarted = false;
309 connectorStatus.transactionStarted = false;
310 delete connectorStatus?.transactionStart;
311 delete connectorStatus?.transactionId;
312 delete connectorStatus?.localAuthorizeIdTag;
313 delete connectorStatus?.authorizeIdTag;
314 delete connectorStatus?.transactionIdTag;
315 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
316 delete connectorStatus?.transactionBeginMeterValue;
317 };
318
319 export const createBootNotificationRequest = (
320 stationInfo: ChargingStationInfo,
321 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp,
322 ): BootNotificationRequest => {
323 const ocppVersion = stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
324 switch (ocppVersion) {
325 case OCPPVersion.VERSION_16:
326 return {
327 chargePointModel: stationInfo.chargePointModel,
328 chargePointVendor: stationInfo.chargePointVendor,
329 ...(!isUndefined(stationInfo.chargeBoxSerialNumber) && {
330 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber,
331 }),
332 ...(!isUndefined(stationInfo.chargePointSerialNumber) && {
333 chargePointSerialNumber: stationInfo.chargePointSerialNumber,
334 }),
335 ...(!isUndefined(stationInfo.firmwareVersion) && {
336 firmwareVersion: stationInfo.firmwareVersion,
337 }),
338 ...(!isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
339 ...(!isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
340 ...(!isUndefined(stationInfo.meterSerialNumber) && {
341 meterSerialNumber: stationInfo.meterSerialNumber,
342 }),
343 ...(!isUndefined(stationInfo.meterType) && {
344 meterType: stationInfo.meterType,
345 }),
346 } as OCPP16BootNotificationRequest;
347 case OCPPVersion.VERSION_20:
348 case OCPPVersion.VERSION_201:
349 return {
350 reason: bootReason,
351 chargingStation: {
352 model: stationInfo.chargePointModel,
353 vendorName: stationInfo.chargePointVendor,
354 ...(!isUndefined(stationInfo.firmwareVersion) && {
355 firmwareVersion: stationInfo.firmwareVersion,
356 }),
357 ...(!isUndefined(stationInfo.chargeBoxSerialNumber) && {
358 serialNumber: stationInfo.chargeBoxSerialNumber,
359 }),
360 ...((!isUndefined(stationInfo.iccid) || !isUndefined(stationInfo.imsi)) && {
361 modem: {
362 ...(!isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
363 ...(!isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
364 },
365 }),
366 },
367 } as OCPP20BootNotificationRequest;
368 }
369 };
370
371 export const warnTemplateKeysDeprecation = (
372 stationTemplate: ChargingStationTemplate,
373 logPrefix: string,
374 templateFile: string,
375 ) => {
376 const templateKeys: { deprecatedKey: string; key?: string }[] = [
377 { deprecatedKey: 'supervisionUrl', key: 'supervisionUrls' },
378 { deprecatedKey: 'authorizationFile', key: 'idTagsFile' },
379 { deprecatedKey: 'payloadSchemaValidation', key: 'ocppStrictCompliance' },
380 { deprecatedKey: 'mustAuthorizeAtRemoteStart', key: 'remoteAuthorization' },
381 ];
382 for (const templateKey of templateKeys) {
383 warnDeprecatedTemplateKey(
384 stationTemplate,
385 templateKey.deprecatedKey,
386 logPrefix,
387 templateFile,
388 !isUndefined(templateKey.key) ? `Use '${templateKey.key}' instead` : undefined,
389 );
390 convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key);
391 }
392 };
393
394 export const stationTemplateToStationInfo = (
395 stationTemplate: ChargingStationTemplate,
396 ): ChargingStationInfo => {
397 stationTemplate = cloneObject<ChargingStationTemplate>(stationTemplate);
398 delete stationTemplate.power;
399 delete stationTemplate.powerUnit;
400 delete stationTemplate.Connectors;
401 delete stationTemplate.Evses;
402 delete stationTemplate.Configuration;
403 delete stationTemplate.AutomaticTransactionGenerator;
404 delete stationTemplate.chargeBoxSerialNumberPrefix;
405 delete stationTemplate.chargePointSerialNumberPrefix;
406 delete stationTemplate.meterSerialNumberPrefix;
407 return stationTemplate as ChargingStationInfo;
408 };
409
410 export const createSerialNumber = (
411 stationTemplate: ChargingStationTemplate,
412 stationInfo: ChargingStationInfo,
413 params: {
414 randomSerialNumberUpperCase?: boolean;
415 randomSerialNumber?: boolean;
416 } = {
417 randomSerialNumberUpperCase: true,
418 randomSerialNumber: true,
419 },
420 ): void => {
421 params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params };
422 const serialNumberSuffix = params?.randomSerialNumber
423 ? getRandomSerialNumberSuffix({
424 upperCase: params.randomSerialNumberUpperCase,
425 })
426 : '';
427 isNotEmptyString(stationTemplate?.chargePointSerialNumberPrefix) &&
428 (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
429 isNotEmptyString(stationTemplate?.chargeBoxSerialNumberPrefix) &&
430 (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
431 isNotEmptyString(stationTemplate?.meterSerialNumberPrefix) &&
432 (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
433 };
434
435 export const propagateSerialNumber = (
436 stationTemplate: ChargingStationTemplate,
437 stationInfoSrc: ChargingStationInfo,
438 stationInfoDst: ChargingStationInfo,
439 ) => {
440 if (!stationInfoSrc || !stationTemplate) {
441 throw new BaseError(
442 'Missing charging station template or existing configuration to propagate serial number',
443 );
444 }
445 stationTemplate?.chargePointSerialNumberPrefix && stationInfoSrc?.chargePointSerialNumber
446 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
447 : stationInfoDst?.chargePointSerialNumber && delete stationInfoDst.chargePointSerialNumber;
448 stationTemplate?.chargeBoxSerialNumberPrefix && stationInfoSrc?.chargeBoxSerialNumber
449 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
450 : stationInfoDst?.chargeBoxSerialNumber && delete stationInfoDst.chargeBoxSerialNumber;
451 stationTemplate?.meterSerialNumberPrefix && stationInfoSrc?.meterSerialNumber
452 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
453 : stationInfoDst?.meterSerialNumber && delete stationInfoDst.meterSerialNumber;
454 };
455
456 export const hasFeatureProfile = (
457 chargingStation: ChargingStation,
458 featureProfile: SupportedFeatureProfiles,
459 ): boolean | undefined => {
460 return getConfigurationKey(
461 chargingStation,
462 StandardParametersKey.SupportedFeatureProfiles,
463 )?.value?.includes(featureProfile);
464 };
465
466 export const getAmperageLimitationUnitDivider = (stationInfo: ChargingStationInfo): number => {
467 let unitDivider = 1;
468 switch (stationInfo.amperageLimitationUnit) {
469 case AmpereUnits.DECI_AMPERE:
470 unitDivider = 10;
471 break;
472 case AmpereUnits.CENTI_AMPERE:
473 unitDivider = 100;
474 break;
475 case AmpereUnits.MILLI_AMPERE:
476 unitDivider = 1000;
477 break;
478 }
479 return unitDivider;
480 };
481
482 export const getChargingStationConnectorChargingProfilesPowerLimit = (
483 chargingStation: ChargingStation,
484 connectorId: number,
485 ): number | undefined => {
486 let limit: number | undefined, matchingChargingProfile: ChargingProfile | undefined;
487 // Get charging profiles for connector id and sort by stack level
488 const chargingProfiles =
489 cloneObject<ChargingProfile[]>(
490 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles!,
491 )?.sort((a, b) => b.stackLevel - a.stackLevel) ?? [];
492 // Get charging profiles on connector 0 and sort by stack level
493 if (isNotEmptyArray(chargingStation.getConnectorStatus(0)?.chargingProfiles)) {
494 chargingProfiles.push(
495 ...cloneObject<ChargingProfile[]>(
496 chargingStation.getConnectorStatus(0)!.chargingProfiles!,
497 ).sort((a, b) => b.stackLevel - a.stackLevel),
498 );
499 }
500 if (isNotEmptyArray(chargingProfiles)) {
501 const result = getLimitFromChargingProfiles(
502 chargingStation,
503 connectorId,
504 chargingProfiles,
505 chargingStation.logPrefix(),
506 );
507 if (!isNullOrUndefined(result)) {
508 limit = result?.limit;
509 matchingChargingProfile = result?.matchingChargingProfile;
510 switch (chargingStation.getCurrentOutType()) {
511 case CurrentType.AC:
512 limit =
513 matchingChargingProfile?.chargingSchedule?.chargingRateUnit ===
514 ChargingRateUnitType.WATT
515 ? limit
516 : ACElectricUtils.powerTotal(
517 chargingStation.getNumberOfPhases(),
518 chargingStation.getVoltageOut(),
519 limit!,
520 );
521 break;
522 case CurrentType.DC:
523 limit =
524 matchingChargingProfile?.chargingSchedule?.chargingRateUnit ===
525 ChargingRateUnitType.WATT
526 ? limit
527 : DCElectricUtils.power(chargingStation.getVoltageOut(), limit!);
528 }
529 const connectorMaximumPower =
530 chargingStation.getMaximumPower() / chargingStation.powerDivider;
531 if (limit! > connectorMaximumPower) {
532 logger.error(
533 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${matchingChargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
534 result,
535 );
536 limit = connectorMaximumPower;
537 }
538 }
539 }
540 return limit;
541 };
542
543 export const getDefaultVoltageOut = (
544 currentType: CurrentType,
545 logPrefix: string,
546 templateFile: string,
547 ): Voltage => {
548 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
549 let defaultVoltageOut: number;
550 switch (currentType) {
551 case CurrentType.AC:
552 defaultVoltageOut = Voltage.VOLTAGE_230;
553 break;
554 case CurrentType.DC:
555 defaultVoltageOut = Voltage.VOLTAGE_400;
556 break;
557 default:
558 logger.error(`${logPrefix} ${errorMsg}`);
559 throw new BaseError(errorMsg);
560 }
561 return defaultVoltageOut;
562 };
563
564 export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
565 return (
566 stationInfo.idTagsFile &&
567 join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
568 );
569 };
570
571 export const waitChargingStationEvents = async (
572 emitter: EventEmitter,
573 event: ChargingStationWorkerMessageEvents,
574 eventsToWait: number,
575 ): Promise<number> => {
576 return new Promise<number>((resolve) => {
577 let events = 0;
578 if (eventsToWait === 0) {
579 resolve(events);
580 }
581 emitter.on(event, () => {
582 ++events;
583 if (events === eventsToWait) {
584 resolve(events);
585 }
586 });
587 });
588 };
589
590 const getConfiguredNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
591 let configuredMaxConnectors = 0;
592 if (isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
593 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
594 configuredMaxConnectors =
595 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)];
596 } else if (isUndefined(stationTemplate.numberOfConnectors) === false) {
597 configuredMaxConnectors = stationTemplate.numberOfConnectors as number;
598 } else if (stationTemplate.Connectors && !stationTemplate.Evses) {
599 configuredMaxConnectors = stationTemplate.Connectors[0]
600 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
601 : getMaxNumberOfConnectors(stationTemplate.Connectors);
602 } else if (stationTemplate.Evses && !stationTemplate.Connectors) {
603 for (const evse in stationTemplate.Evses) {
604 if (evse === '0') {
605 continue;
606 }
607 configuredMaxConnectors += getMaxNumberOfConnectors(stationTemplate.Evses[evse].Connectors);
608 }
609 }
610 return configuredMaxConnectors;
611 };
612
613 const checkConfiguredMaxConnectors = (
614 configuredMaxConnectors: number,
615 logPrefix: string,
616 templateFile: string,
617 ): void => {
618 if (configuredMaxConnectors <= 0) {
619 logger.warn(
620 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
621 );
622 }
623 };
624
625 const checkTemplateMaxConnectors = (
626 templateMaxConnectors: number,
627 logPrefix: string,
628 templateFile: string,
629 ): void => {
630 if (templateMaxConnectors === 0) {
631 logger.warn(
632 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
633 );
634 } else if (templateMaxConnectors < 0) {
635 logger.error(
636 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
637 );
638 }
639 };
640
641 const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
642 connectorStatus.availability = AvailabilityType.Operative;
643 connectorStatus.idTagLocalAuthorized = false;
644 connectorStatus.idTagAuthorized = false;
645 connectorStatus.transactionRemoteStarted = false;
646 connectorStatus.transactionStarted = false;
647 connectorStatus.energyActiveImportRegisterValue = 0;
648 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
649 if (isUndefined(connectorStatus.chargingProfiles)) {
650 connectorStatus.chargingProfiles = [];
651 }
652 };
653
654 const warnDeprecatedTemplateKey = (
655 template: ChargingStationTemplate,
656 key: string,
657 logPrefix: string,
658 templateFile: string,
659 logMsgToAppend = '',
660 ): void => {
661 if (!isUndefined(template[key as keyof ChargingStationTemplate])) {
662 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
663 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
664 }`;
665 logger.warn(`${logPrefix} ${logMsg}`);
666 console.warn(chalk.yellow(`${logMsg}`));
667 }
668 };
669
670 const convertDeprecatedTemplateKey = (
671 template: ChargingStationTemplate,
672 deprecatedKey: string,
673 key?: string,
674 ): void => {
675 if (!isUndefined(template[deprecatedKey as keyof ChargingStationTemplate])) {
676 if (!isUndefined(key)) {
677 (template as unknown as Record<string, unknown>)[key!] =
678 template[deprecatedKey as keyof ChargingStationTemplate];
679 }
680 delete template[deprecatedKey as keyof ChargingStationTemplate];
681 }
682 };
683
684 interface ChargingProfilesLimit {
685 limit: number;
686 matchingChargingProfile: ChargingProfile;
687 }
688
689 /**
690 * Charging profiles shall already be sorted by connector id and stack level (highest stack level has priority)
691 *
692 * @param chargingStation -
693 * @param connectorId -
694 * @param chargingProfiles -
695 * @param logPrefix -
696 * @returns ChargingProfilesLimit
697 */
698 const getLimitFromChargingProfiles = (
699 chargingStation: ChargingStation,
700 connectorId: number,
701 chargingProfiles: ChargingProfile[],
702 logPrefix: string,
703 ): ChargingProfilesLimit | undefined => {
704 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
705 const currentDate = new Date();
706 const connectorStatus = chargingStation.getConnectorStatus(connectorId);
707 for (const chargingProfile of chargingProfiles) {
708 const chargingSchedule = chargingProfile.chargingSchedule;
709 if (connectorStatus?.transactionStarted && isNullOrUndefined(chargingSchedule?.startSchedule)) {
710 logger.debug(
711 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`,
712 );
713 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
714 chargingSchedule.startSchedule = connectorStatus?.transactionStart;
715 }
716 if (!isDate(chargingSchedule?.startSchedule)) {
717 logger.warn(
718 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date object. Trying to convert it to a Date object`,
719 );
720 chargingSchedule.startSchedule = convertToDate(chargingSchedule?.startSchedule)!;
721 }
722 switch (chargingProfile.chargingProfileKind) {
723 case ChargingProfileKindType.RECURRING:
724 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
725 continue;
726 }
727 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix);
728 break;
729 case ChargingProfileKindType.RELATIVE:
730 connectorStatus?.transactionStarted &&
731 (chargingSchedule.startSchedule = connectorStatus?.transactionStart);
732 break;
733 }
734 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
735 continue;
736 }
737 // Check if the charging profile is active
738 if (
739 isValidTime(chargingSchedule?.startSchedule) &&
740 isWithinInterval(currentDate, {
741 start: chargingSchedule.startSchedule!,
742 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
743 })
744 ) {
745 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
746 const chargingSchedulePeriodCompareFn = (
747 a: ChargingSchedulePeriod,
748 b: ChargingSchedulePeriod,
749 ) => a.startPeriod - b.startPeriod;
750 if (
751 isArraySorted<ChargingSchedulePeriod>(
752 chargingSchedule.chargingSchedulePeriod,
753 chargingSchedulePeriodCompareFn,
754 ) === false
755 ) {
756 logger.warn(
757 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
758 );
759 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn);
760 }
761 // Check if the first schedule period start period is equal to 0
762 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
763 logger.error(
764 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`,
765 );
766 continue;
767 }
768 // Handle only one schedule period
769 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
770 const result: ChargingProfilesLimit = {
771 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
772 matchingChargingProfile: chargingProfile,
773 };
774 logger.debug(debugLogMsg, result);
775 return result;
776 }
777 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined;
778 // Search for the right schedule period
779 for (const [
780 index,
781 chargingSchedulePeriod,
782 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
783 // Find the right schedule period
784 if (
785 isAfter(
786 addSeconds(chargingSchedule.startSchedule!, chargingSchedulePeriod.startPeriod),
787 currentDate,
788 )
789 ) {
790 // Found the schedule period: previous is the correct one
791 const result: ChargingProfilesLimit = {
792 limit: previousChargingSchedulePeriod!.limit,
793 matchingChargingProfile: chargingProfile,
794 };
795 logger.debug(debugLogMsg, result);
796 return result;
797 }
798 // Keep a reference to previous one
799 previousChargingSchedulePeriod = chargingSchedulePeriod;
800 // Handle the last schedule period within the charging profile duration
801 if (
802 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
803 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
804 chargingSchedule.duration! >
805 differenceInSeconds(
806 addSeconds(
807 chargingSchedule.startSchedule!,
808 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
809 ),
810 chargingSchedule.startSchedule!,
811 ))
812 ) {
813 const result: ChargingProfilesLimit = {
814 limit: previousChargingSchedulePeriod.limit,
815 matchingChargingProfile: chargingProfile,
816 };
817 logger.debug(debugLogMsg, result);
818 return result;
819 }
820 }
821 }
822 }
823 }
824 };
825
826 const canProceedChargingProfile = (
827 chargingProfile: ChargingProfile,
828 currentDate: Date,
829 logPrefix: string,
830 ): boolean => {
831 if (
832 (isValidTime(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom!)) ||
833 (isValidTime(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo!))
834 ) {
835 logger.debug(
836 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
837 chargingProfile.chargingProfileId
838 } is not valid for the current date ${currentDate.toISOString()}`,
839 );
840 return false;
841 }
842 const chargingSchedule = chargingProfile.chargingSchedule;
843 if (isNullOrUndefined(chargingSchedule?.startSchedule)) {
844 logger.error(
845 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has (still) no startSchedule defined`,
846 );
847 return false;
848 }
849 if (isNullOrUndefined(chargingSchedule?.duration)) {
850 logger.error(
851 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined, not yet supported`,
852 );
853 return false;
854 }
855 return true;
856 };
857
858 const canProceedRecurringChargingProfile = (
859 chargingProfile: ChargingProfile,
860 logPrefix: string,
861 ): boolean => {
862 if (
863 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
864 isNullOrUndefined(chargingProfile.recurrencyKind)
865 ) {
866 logger.error(
867 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
868 );
869 return false;
870 }
871 return true;
872 };
873
874 /**
875 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
876 *
877 * @param chargingProfile -
878 * @param currentDate -
879 * @param logPrefix -
880 */
881 const prepareRecurringChargingProfile = (
882 chargingProfile: ChargingProfile,
883 currentDate: Date,
884 logPrefix: string,
885 ): boolean => {
886 const chargingSchedule = chargingProfile.chargingSchedule;
887 let recurringIntervalTranslated = false;
888 let recurringInterval: Interval;
889 switch (chargingProfile.recurrencyKind) {
890 case RecurrencyKindType.DAILY:
891 recurringInterval = {
892 start: chargingSchedule.startSchedule!,
893 end: addDays(chargingSchedule.startSchedule!, 1),
894 };
895 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
896 if (
897 !isWithinInterval(currentDate, recurringInterval) &&
898 isBefore(recurringInterval.end, currentDate)
899 ) {
900 chargingSchedule.startSchedule = addDays(
901 recurringInterval.start,
902 differenceInDays(currentDate, recurringInterval.start),
903 );
904 recurringInterval = {
905 start: chargingSchedule.startSchedule,
906 end: addDays(chargingSchedule.startSchedule, 1),
907 };
908 recurringIntervalTranslated = true;
909 }
910 break;
911 case RecurrencyKindType.WEEKLY:
912 recurringInterval = {
913 start: chargingSchedule.startSchedule!,
914 end: addWeeks(chargingSchedule.startSchedule!, 1),
915 };
916 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
917 if (
918 !isWithinInterval(currentDate, recurringInterval) &&
919 isBefore(recurringInterval.end, currentDate)
920 ) {
921 chargingSchedule.startSchedule = addWeeks(
922 recurringInterval.start,
923 differenceInWeeks(currentDate, recurringInterval.start),
924 );
925 recurringInterval = {
926 start: chargingSchedule.startSchedule,
927 end: addWeeks(chargingSchedule.startSchedule, 1),
928 };
929 recurringIntervalTranslated = true;
930 }
931 break;
932 default:
933 logger.error(
934 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} recurrency kind ${chargingProfile.recurrencyKind} is not supported`,
935 );
936 }
937 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
938 logger.error(
939 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
940 chargingProfile.recurrencyKind
941 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
942 recurringInterval!.start,
943 ).toISOString()}, ${toDate(
944 recurringInterval!.end,
945 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
946 );
947 }
948 return recurringIntervalTranslated;
949 };
950
951 const checkRecurringChargingProfileDuration = (
952 chargingProfile: ChargingProfile,
953 interval: Interval,
954 logPrefix: string,
955 ): void => {
956 if (isNullOrUndefined(chargingProfile.chargingSchedule.duration)) {
957 logger.warn(
958 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
959 chargingProfile.chargingProfileKind
960 } charging profile id ${
961 chargingProfile.chargingProfileId
962 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
963 interval.end,
964 interval.start,
965 )}`,
966 );
967 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
968 } else if (
969 chargingProfile.chargingSchedule.duration! > differenceInSeconds(interval.end, interval.start)
970 ) {
971 logger.warn(
972 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
973 chargingProfile.chargingProfileKind
974 } charging profile id ${chargingProfile.chargingProfileId} duration ${
975 chargingProfile.chargingSchedule.duration
976 } is greater than the recurrency time interval duration ${differenceInSeconds(
977 interval.end,
978 interval.start,
979 )}`,
980 );
981 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
982 }
983 };
984
985 const getRandomSerialNumberSuffix = (params?: {
986 randomBytesLength?: number;
987 upperCase?: boolean;
988 }): string => {
989 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex');
990 if (params?.upperCase) {
991 return randomSerialNumberSuffix.toUpperCase();
992 }
993 return randomSerialNumberSuffix;
994 };