05773f0159f3f1378dd78af4cf2a46ac3395b373
[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 isPast,
18 isWithinInterval,
19 toDate,
20 } from 'date-fns';
21
22 import type { ChargingStation } from './ChargingStation';
23 import { getConfigurationKey } from './ConfigurationKeyUtils';
24 import { BaseError } from '../exception';
25 import {
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';
52 import {
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
71 const moduleName = 'Helpers';
72
73 export 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
88 export const hasReservationExpired = (reservation: Reservation): boolean => {
89 return isPast(reservation.expiryDate);
90 };
91
92 export 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
118 export 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
133 export 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
155 export 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
166 export 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
183 export const getMaxNumberOfEvses = (evses: Record<string, EvseTemplate>): number => {
184 if (!evses) {
185 return -1;
186 }
187 return Object.keys(evses).length;
188 };
189
190 const getMaxNumberOfConnectors = (connectors: Record<string, ConnectorStatus>): number => {
191 if (!connectors) {
192 return -1;
193 }
194 return Object.keys(connectors).length;
195 };
196
197 export 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
222 export 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
251 export 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
279 export 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
293 export 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
314 export 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
340 export 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
354 export 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
406 export 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
429 export 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
445 export 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
470 export 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
491 export 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
501 export 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
517 export 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
571 export 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
592 export 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
599 export 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
618 const 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
643 const 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
655 const 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
671 const 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
684 const 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
700 const 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
714 interface 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 */
728 const 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 switch (chargingProfile.chargingProfileKind) {
759 case ChargingProfileKindType.RECURRING:
760 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
761 continue;
762 }
763 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix);
764 break;
765 case ChargingProfileKindType.RELATIVE:
766 connectorStatus?.transactionStarted &&
767 (chargingSchedule.startSchedule = connectorStatus?.transactionStart);
768 break;
769 }
770 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
771 continue;
772 }
773 // Check if the charging profile is active
774 if (
775 isValidTime(chargingSchedule?.startSchedule) &&
776 isWithinInterval(currentDate, {
777 start: chargingSchedule.startSchedule!,
778 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
779 })
780 ) {
781 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
782 const chargingSchedulePeriodCompareFn = (
783 a: ChargingSchedulePeriod,
784 b: ChargingSchedulePeriod,
785 ) => a.startPeriod - b.startPeriod;
786 if (
787 isArraySorted<ChargingSchedulePeriod>(
788 chargingSchedule.chargingSchedulePeriod,
789 chargingSchedulePeriodCompareFn,
790 ) === false
791 ) {
792 logger.warn(
793 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
794 );
795 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn);
796 }
797 // Check if the first schedule period start period is equal to 0
798 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
799 logger.error(
800 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`,
801 );
802 continue;
803 }
804 // Handle only one schedule period
805 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
806 const result: ChargingProfilesLimit = {
807 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
808 matchingChargingProfile: chargingProfile,
809 };
810 logger.debug(debugLogMsg, result);
811 return result;
812 }
813 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined;
814 // Search for the right schedule period
815 for (const [
816 index,
817 chargingSchedulePeriod,
818 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
819 // Find the right schedule period
820 if (
821 isAfter(
822 addSeconds(chargingSchedule.startSchedule!, chargingSchedulePeriod.startPeriod),
823 currentDate,
824 )
825 ) {
826 // Found the schedule period: previous is the correct one
827 const result: ChargingProfilesLimit = {
828 limit: previousChargingSchedulePeriod!.limit,
829 matchingChargingProfile: chargingProfile,
830 };
831 logger.debug(debugLogMsg, result);
832 return result;
833 }
834 // Keep a reference to previous one
835 previousChargingSchedulePeriod = chargingSchedulePeriod;
836 // Handle the last schedule period within the charging profile duration
837 if (
838 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
839 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
840 chargingSchedule.duration! >
841 differenceInSeconds(
842 addSeconds(
843 chargingSchedule.startSchedule!,
844 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
845 ),
846 chargingSchedule.startSchedule!,
847 ))
848 ) {
849 const result: ChargingProfilesLimit = {
850 limit: previousChargingSchedulePeriod.limit,
851 matchingChargingProfile: chargingProfile,
852 };
853 logger.debug(debugLogMsg, result);
854 return result;
855 }
856 }
857 }
858 }
859 }
860 };
861
862 export const canProceedChargingProfile = (
863 chargingProfile: ChargingProfile,
864 currentDate: Date,
865 logPrefix: string,
866 ): boolean => {
867 if (
868 (isValidTime(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom!)) ||
869 (isValidTime(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo!))
870 ) {
871 logger.debug(
872 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
873 chargingProfile.chargingProfileId
874 } is not valid for the current date ${currentDate.toISOString()}`,
875 );
876 return false;
877 }
878 const chargingSchedule = chargingProfile.chargingSchedule;
879 if (isNullOrUndefined(chargingSchedule?.startSchedule)) {
880 logger.error(
881 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`,
882 );
883 return false;
884 }
885 if (isNullOrUndefined(chargingSchedule?.duration)) {
886 logger.error(
887 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined, not yet supported`,
888 );
889 return false;
890 }
891 return true;
892 };
893
894 export const canProceedRecurringChargingProfile = (
895 chargingProfile: ChargingProfile,
896 logPrefix: string,
897 ): boolean => {
898 if (
899 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
900 isNullOrUndefined(chargingProfile.recurrencyKind)
901 ) {
902 logger.error(
903 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
904 );
905 return false;
906 }
907 return true;
908 };
909
910 /**
911 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
912 *
913 * @param chargingProfile -
914 * @param currentDate -
915 * @param logPrefix -
916 */
917 export const prepareRecurringChargingProfile = (
918 chargingProfile: ChargingProfile,
919 currentDate: Date,
920 logPrefix: string,
921 ): boolean => {
922 const chargingSchedule = chargingProfile.chargingSchedule;
923 let recurringIntervalTranslated = false;
924 let recurringInterval: Interval;
925 switch (chargingProfile.recurrencyKind) {
926 case RecurrencyKindType.DAILY:
927 recurringInterval = {
928 start: chargingSchedule.startSchedule!,
929 end: addDays(chargingSchedule.startSchedule!, 1),
930 };
931 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
932 if (
933 !isWithinInterval(currentDate, recurringInterval) &&
934 isBefore(recurringInterval.end, currentDate)
935 ) {
936 chargingSchedule.startSchedule = addDays(
937 recurringInterval.start,
938 differenceInDays(currentDate, recurringInterval.start),
939 );
940 recurringInterval = {
941 start: chargingSchedule.startSchedule,
942 end: addDays(chargingSchedule.startSchedule, 1),
943 };
944 recurringIntervalTranslated = true;
945 }
946 break;
947 case RecurrencyKindType.WEEKLY:
948 recurringInterval = {
949 start: chargingSchedule.startSchedule!,
950 end: addWeeks(chargingSchedule.startSchedule!, 1),
951 };
952 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
953 if (
954 !isWithinInterval(currentDate, recurringInterval) &&
955 isBefore(recurringInterval.end, currentDate)
956 ) {
957 chargingSchedule.startSchedule = addWeeks(
958 recurringInterval.start,
959 differenceInWeeks(currentDate, recurringInterval.start),
960 );
961 recurringInterval = {
962 start: chargingSchedule.startSchedule,
963 end: addWeeks(chargingSchedule.startSchedule, 1),
964 };
965 recurringIntervalTranslated = true;
966 }
967 break;
968 default:
969 logger.error(
970 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} recurrency kind ${chargingProfile.recurrencyKind} is not supported`,
971 );
972 }
973 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
974 logger.error(
975 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
976 chargingProfile.recurrencyKind
977 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
978 recurringInterval!.start,
979 ).toISOString()}, ${toDate(
980 recurringInterval!.end,
981 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
982 );
983 }
984 return recurringIntervalTranslated;
985 };
986
987 const checkRecurringChargingProfileDuration = (
988 chargingProfile: ChargingProfile,
989 interval: Interval,
990 logPrefix: string,
991 ): void => {
992 if (isNullOrUndefined(chargingProfile.chargingSchedule.duration)) {
993 logger.warn(
994 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
995 chargingProfile.chargingProfileKind
996 } charging profile id ${
997 chargingProfile.chargingProfileId
998 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
999 interval.end,
1000 interval.start,
1001 )}`,
1002 );
1003 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
1004 } else if (
1005 chargingProfile.chargingSchedule.duration! > differenceInSeconds(interval.end, interval.start)
1006 ) {
1007 logger.warn(
1008 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1009 chargingProfile.chargingProfileKind
1010 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1011 chargingProfile.chargingSchedule.duration
1012 } is greater than the recurrency time interval duration ${differenceInSeconds(
1013 interval.end,
1014 interval.start,
1015 )}`,
1016 );
1017 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
1018 }
1019 };
1020
1021 const getRandomSerialNumberSuffix = (params?: {
1022 randomBytesLength?: number;
1023 upperCase?: boolean;
1024 }): string => {
1025 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex');
1026 if (params?.upperCase) {
1027 return randomSerialNumberSuffix.toUpperCase();
1028 }
1029 return randomSerialNumberSuffix;
1030 };