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