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