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