fix: avoid overlapping schedule periods in get composite schedule
[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, chargingProfile: 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 chargingProfile = result?.chargingProfile;
538 switch (chargingStation.getCurrentOutType()) {
539 case CurrentType.AC:
540 limit =
541 chargingProfile?.chargingSchedule?.chargingRateUnit === ChargingRateUnitType.WATT
542 ? limit
543 : ACElectricUtils.powerTotal(
544 chargingStation.getNumberOfPhases(),
545 chargingStation.getVoltageOut(),
546 limit!,
547 );
548 break;
549 case CurrentType.DC:
550 limit =
551 chargingProfile?.chargingSchedule?.chargingRateUnit === ChargingRateUnitType.WATT
552 ? limit
553 : DCElectricUtils.power(chargingStation.getVoltageOut(), limit!);
554 }
555 const connectorMaximumPower =
556 chargingStation.getMaximumPower() / chargingStation.powerDivider;
557 if (limit! > connectorMaximumPower) {
558 logger.error(
559 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${chargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
560 result,
561 );
562 limit = connectorMaximumPower;
563 }
564 }
565 }
566 return limit;
567 };
568
569 export const getDefaultVoltageOut = (
570 currentType: CurrentType,
571 logPrefix: string,
572 templateFile: string,
573 ): Voltage => {
574 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
575 let defaultVoltageOut: number;
576 switch (currentType) {
577 case CurrentType.AC:
578 defaultVoltageOut = Voltage.VOLTAGE_230;
579 break;
580 case CurrentType.DC:
581 defaultVoltageOut = Voltage.VOLTAGE_400;
582 break;
583 default:
584 logger.error(`${logPrefix} ${errorMsg}`);
585 throw new BaseError(errorMsg);
586 }
587 return defaultVoltageOut;
588 };
589
590 export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
591 return (
592 stationInfo.idTagsFile &&
593 join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
594 );
595 };
596
597 export const waitChargingStationEvents = async (
598 emitter: EventEmitter,
599 event: ChargingStationWorkerMessageEvents,
600 eventsToWait: number,
601 ): Promise<number> => {
602 return new Promise<number>((resolve) => {
603 let events = 0;
604 if (eventsToWait === 0) {
605 resolve(events);
606 }
607 emitter.on(event, () => {
608 ++events;
609 if (events === eventsToWait) {
610 resolve(events);
611 }
612 });
613 });
614 };
615
616 const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
617 let configuredMaxNumberOfConnectors = 0;
618 if (isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
619 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
620 configuredMaxNumberOfConnectors =
621 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)];
622 } else if (isUndefined(stationTemplate.numberOfConnectors) === false) {
623 configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors as number;
624 } else if (stationTemplate.Connectors && !stationTemplate.Evses) {
625 configuredMaxNumberOfConnectors = stationTemplate.Connectors[0]
626 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
627 : getMaxNumberOfConnectors(stationTemplate.Connectors);
628 } else if (stationTemplate.Evses && !stationTemplate.Connectors) {
629 for (const evse in stationTemplate.Evses) {
630 if (evse === '0') {
631 continue;
632 }
633 configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
634 stationTemplate.Evses[evse].Connectors,
635 );
636 }
637 }
638 return configuredMaxNumberOfConnectors;
639 };
640
641 const checkConfiguredMaxConnectors = (
642 configuredMaxConnectors: number,
643 logPrefix: string,
644 templateFile: string,
645 ): void => {
646 if (configuredMaxConnectors <= 0) {
647 logger.warn(
648 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
649 );
650 }
651 };
652
653 const checkTemplateMaxConnectors = (
654 templateMaxConnectors: number,
655 logPrefix: string,
656 templateFile: string,
657 ): void => {
658 if (templateMaxConnectors === 0) {
659 logger.warn(
660 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
661 );
662 } else if (templateMaxConnectors < 0) {
663 logger.error(
664 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
665 );
666 }
667 };
668
669 const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
670 connectorStatus.availability = AvailabilityType.Operative;
671 connectorStatus.idTagLocalAuthorized = false;
672 connectorStatus.idTagAuthorized = false;
673 connectorStatus.transactionRemoteStarted = false;
674 connectorStatus.transactionStarted = false;
675 connectorStatus.energyActiveImportRegisterValue = 0;
676 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
677 if (isUndefined(connectorStatus.chargingProfiles)) {
678 connectorStatus.chargingProfiles = [];
679 }
680 };
681
682 const warnDeprecatedTemplateKey = (
683 template: ChargingStationTemplate,
684 key: string,
685 logPrefix: string,
686 templateFile: string,
687 logMsgToAppend = '',
688 ): void => {
689 if (!isUndefined(template[key as keyof ChargingStationTemplate])) {
690 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
691 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
692 }`;
693 logger.warn(`${logPrefix} ${logMsg}`);
694 console.warn(chalk.yellow(`${logMsg}`));
695 }
696 };
697
698 const convertDeprecatedTemplateKey = (
699 template: ChargingStationTemplate,
700 deprecatedKey: string,
701 key?: string,
702 ): void => {
703 if (!isUndefined(template[deprecatedKey as keyof ChargingStationTemplate])) {
704 if (!isUndefined(key)) {
705 (template as unknown as Record<string, unknown>)[key!] =
706 template[deprecatedKey as keyof ChargingStationTemplate];
707 }
708 delete template[deprecatedKey as keyof ChargingStationTemplate];
709 }
710 };
711
712 interface ChargingProfilesLimit {
713 limit: number;
714 chargingProfile: ChargingProfile;
715 }
716
717 /**
718 * Charging profiles shall already be sorted by connector id and stack level (highest stack level has priority)
719 *
720 * @param chargingStation -
721 * @param connectorId -
722 * @param chargingProfiles -
723 * @param logPrefix -
724 * @returns ChargingProfilesLimit
725 */
726 const getLimitFromChargingProfiles = (
727 chargingStation: ChargingStation,
728 connectorId: number,
729 chargingProfiles: ChargingProfile[],
730 logPrefix: string,
731 ): ChargingProfilesLimit | undefined => {
732 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
733 const currentDate = new Date();
734 const connectorStatus = chargingStation.getConnectorStatus(connectorId)!;
735 if (!isArraySorted(chargingProfiles, (a, b) => b.stackLevel - a.stackLevel)) {
736 logger.warn(
737 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profiles are not sorted by stack level. Trying to sort them`,
738 );
739 chargingProfiles.sort((a, b) => b.stackLevel - a.stackLevel);
740 }
741 for (const chargingProfile of chargingProfiles) {
742 const chargingSchedule = chargingProfile.chargingSchedule;
743 if (connectorStatus?.transactionStarted && isNullOrUndefined(chargingSchedule?.startSchedule)) {
744 logger.debug(
745 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`,
746 );
747 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
748 chargingSchedule.startSchedule = connectorStatus?.transactionStart;
749 }
750 if (!isDate(chargingSchedule?.startSchedule)) {
751 logger.warn(
752 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date object. Trying to convert it to a Date object`,
753 );
754 chargingSchedule.startSchedule = convertToDate(chargingSchedule?.startSchedule)!;
755 }
756 if (!prepareChargingProfileKind(connectorStatus, chargingProfile, currentDate, logPrefix)) {
757 continue;
758 }
759 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
760 continue;
761 }
762 // Check if the charging profile is active
763 if (
764 isValidTime(chargingSchedule?.startSchedule) &&
765 isWithinInterval(currentDate, {
766 start: chargingSchedule.startSchedule!,
767 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
768 })
769 ) {
770 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
771 const chargingSchedulePeriodCompareFn = (
772 a: ChargingSchedulePeriod,
773 b: ChargingSchedulePeriod,
774 ) => a.startPeriod - b.startPeriod;
775 if (
776 isArraySorted<ChargingSchedulePeriod>(
777 chargingSchedule.chargingSchedulePeriod,
778 chargingSchedulePeriodCompareFn,
779 ) === false
780 ) {
781 logger.warn(
782 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
783 );
784 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn);
785 }
786 // Check if the first schedule period start period is equal to 0
787 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
788 logger.error(
789 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`,
790 );
791 continue;
792 }
793 // Handle only one schedule period
794 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
795 const result: ChargingProfilesLimit = {
796 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
797 chargingProfile,
798 };
799 logger.debug(debugLogMsg, result);
800 return result;
801 }
802 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined;
803 // Search for the right schedule period
804 for (const [
805 index,
806 chargingSchedulePeriod,
807 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
808 // Find the right schedule period
809 if (
810 isAfter(
811 addSeconds(chargingSchedule.startSchedule!, chargingSchedulePeriod.startPeriod),
812 currentDate,
813 )
814 ) {
815 // Found the schedule period: previous is the correct one
816 const result: ChargingProfilesLimit = {
817 limit: previousChargingSchedulePeriod!.limit,
818 chargingProfile: chargingProfile,
819 };
820 logger.debug(debugLogMsg, result);
821 return result;
822 }
823 // Keep a reference to previous one
824 previousChargingSchedulePeriod = chargingSchedulePeriod;
825 // Handle the last schedule period within the charging profile duration
826 if (
827 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
828 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
829 chargingSchedule.duration! >
830 differenceInSeconds(
831 addSeconds(
832 chargingSchedule.startSchedule!,
833 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
834 ),
835 chargingSchedule.startSchedule!,
836 ))
837 ) {
838 const result: ChargingProfilesLimit = {
839 limit: previousChargingSchedulePeriod.limit,
840 chargingProfile: chargingProfile,
841 };
842 logger.debug(debugLogMsg, result);
843 return result;
844 }
845 }
846 }
847 }
848 }
849 };
850
851 export const prepareChargingProfileKind = (
852 connectorStatus: ConnectorStatus,
853 chargingProfile: ChargingProfile,
854 currentDate: Date,
855 logPrefix: string,
856 ): boolean => {
857 switch (chargingProfile.chargingProfileKind) {
858 case ChargingProfileKindType.RECURRING:
859 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
860 return false;
861 }
862 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix);
863 break;
864 case ChargingProfileKindType.RELATIVE:
865 connectorStatus?.transactionStarted &&
866 (chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart);
867 break;
868 }
869 return true;
870 };
871
872 export const canProceedChargingProfile = (
873 chargingProfile: ChargingProfile,
874 currentDate: Date,
875 logPrefix: string,
876 ): boolean => {
877 if (
878 (isValidTime(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom!)) ||
879 (isValidTime(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo!))
880 ) {
881 logger.debug(
882 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
883 chargingProfile.chargingProfileId
884 } is not valid for the current date ${currentDate.toISOString()}`,
885 );
886 return false;
887 }
888 const chargingSchedule = chargingProfile.chargingSchedule;
889 if (isNullOrUndefined(chargingSchedule?.startSchedule)) {
890 logger.error(
891 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`,
892 );
893 return false;
894 }
895 if (isNullOrUndefined(chargingSchedule?.duration)) {
896 logger.error(
897 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined, not yet supported`,
898 );
899 return false;
900 }
901 return true;
902 };
903
904 const canProceedRecurringChargingProfile = (
905 chargingProfile: ChargingProfile,
906 logPrefix: string,
907 ): boolean => {
908 if (
909 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
910 isNullOrUndefined(chargingProfile.recurrencyKind)
911 ) {
912 logger.error(
913 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
914 );
915 return false;
916 }
917 return true;
918 };
919
920 /**
921 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
922 *
923 * @param chargingProfile -
924 * @param currentDate -
925 * @param logPrefix -
926 */
927 const prepareRecurringChargingProfile = (
928 chargingProfile: ChargingProfile,
929 currentDate: Date,
930 logPrefix: string,
931 ): boolean => {
932 const chargingSchedule = chargingProfile.chargingSchedule;
933 let recurringIntervalTranslated = false;
934 let recurringInterval: Interval;
935 switch (chargingProfile.recurrencyKind) {
936 case RecurrencyKindType.DAILY:
937 recurringInterval = {
938 start: chargingSchedule.startSchedule!,
939 end: addDays(chargingSchedule.startSchedule!, 1),
940 };
941 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
942 if (
943 !isWithinInterval(currentDate, recurringInterval) &&
944 isBefore(recurringInterval.end, currentDate)
945 ) {
946 chargingSchedule.startSchedule = addDays(
947 recurringInterval.start,
948 differenceInDays(currentDate, recurringInterval.start),
949 );
950 recurringInterval = {
951 start: chargingSchedule.startSchedule,
952 end: addDays(chargingSchedule.startSchedule, 1),
953 };
954 recurringIntervalTranslated = true;
955 }
956 break;
957 case RecurrencyKindType.WEEKLY:
958 recurringInterval = {
959 start: chargingSchedule.startSchedule!,
960 end: addWeeks(chargingSchedule.startSchedule!, 1),
961 };
962 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
963 if (
964 !isWithinInterval(currentDate, recurringInterval) &&
965 isBefore(recurringInterval.end, currentDate)
966 ) {
967 chargingSchedule.startSchedule = addWeeks(
968 recurringInterval.start,
969 differenceInWeeks(currentDate, recurringInterval.start),
970 );
971 recurringInterval = {
972 start: chargingSchedule.startSchedule,
973 end: addWeeks(chargingSchedule.startSchedule, 1),
974 };
975 recurringIntervalTranslated = true;
976 }
977 break;
978 default:
979 logger.error(
980 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} recurrency kind ${chargingProfile.recurrencyKind} is not supported`,
981 );
982 }
983 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
984 logger.error(
985 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
986 chargingProfile.recurrencyKind
987 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
988 recurringInterval!.start,
989 ).toISOString()}, ${toDate(
990 recurringInterval!.end,
991 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
992 );
993 }
994 return recurringIntervalTranslated;
995 };
996
997 const checkRecurringChargingProfileDuration = (
998 chargingProfile: ChargingProfile,
999 interval: Interval,
1000 logPrefix: string,
1001 ): void => {
1002 if (isNullOrUndefined(chargingProfile.chargingSchedule.duration)) {
1003 logger.warn(
1004 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1005 chargingProfile.chargingProfileKind
1006 } charging profile id ${
1007 chargingProfile.chargingProfileId
1008 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1009 interval.end,
1010 interval.start,
1011 )}`,
1012 );
1013 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
1014 } else if (
1015 chargingProfile.chargingSchedule.duration! > differenceInSeconds(interval.end, interval.start)
1016 ) {
1017 logger.warn(
1018 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1019 chargingProfile.chargingProfileKind
1020 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1021 chargingProfile.chargingSchedule.duration
1022 } is greater than the recurrency time interval duration ${differenceInSeconds(
1023 interval.end,
1024 interval.start,
1025 )}`,
1026 );
1027 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
1028 }
1029 };
1030
1031 const getRandomSerialNumberSuffix = (params?: {
1032 randomBytesLength?: number;
1033 upperCase?: boolean;
1034 }): string => {
1035 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex');
1036 if (params?.upperCase) {
1037 return randomSerialNumberSuffix.toUpperCase();
1038 }
1039 return randomSerialNumberSuffix;
1040 };