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