fix: remove transaction charging profiles at stop transaction
[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 = isNotEmptyArray(connectorStatus.chargingProfiles)
343 ? connectorStatus.chargingProfiles?.filter(
344 (chargingProfile) => chargingProfile.transactionId !== connectorStatus?.transactionId,
345 )
346 : [];
347 connectorStatus.idTagLocalAuthorized = false;
348 connectorStatus.idTagAuthorized = false;
349 connectorStatus.transactionRemoteStarted = false;
350 connectorStatus.transactionStarted = false;
351 delete connectorStatus?.transactionStart;
352 delete connectorStatus?.transactionId;
353 delete connectorStatus?.localAuthorizeIdTag;
354 delete connectorStatus?.authorizeIdTag;
355 delete connectorStatus?.transactionIdTag;
356 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
357 delete connectorStatus?.transactionBeginMeterValue;
358 };
359
360 export const createBootNotificationRequest = (
361 stationInfo: ChargingStationInfo,
362 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp,
363 ): BootNotificationRequest => {
364 const ocppVersion = stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
365 switch (ocppVersion) {
366 case OCPPVersion.VERSION_16:
367 return {
368 chargePointModel: stationInfo.chargePointModel,
369 chargePointVendor: stationInfo.chargePointVendor,
370 ...(!isUndefined(stationInfo.chargeBoxSerialNumber) && {
371 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber,
372 }),
373 ...(!isUndefined(stationInfo.chargePointSerialNumber) && {
374 chargePointSerialNumber: stationInfo.chargePointSerialNumber,
375 }),
376 ...(!isUndefined(stationInfo.firmwareVersion) && {
377 firmwareVersion: stationInfo.firmwareVersion,
378 }),
379 ...(!isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
380 ...(!isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
381 ...(!isUndefined(stationInfo.meterSerialNumber) && {
382 meterSerialNumber: stationInfo.meterSerialNumber,
383 }),
384 ...(!isUndefined(stationInfo.meterType) && {
385 meterType: stationInfo.meterType,
386 }),
387 } as OCPP16BootNotificationRequest;
388 case OCPPVersion.VERSION_20:
389 case OCPPVersion.VERSION_201:
390 return {
391 reason: bootReason,
392 chargingStation: {
393 model: stationInfo.chargePointModel,
394 vendorName: stationInfo.chargePointVendor,
395 ...(!isUndefined(stationInfo.firmwareVersion) && {
396 firmwareVersion: stationInfo.firmwareVersion,
397 }),
398 ...(!isUndefined(stationInfo.chargeBoxSerialNumber) && {
399 serialNumber: stationInfo.chargeBoxSerialNumber,
400 }),
401 ...((!isUndefined(stationInfo.iccid) || !isUndefined(stationInfo.imsi)) && {
402 modem: {
403 ...(!isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
404 ...(!isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
405 },
406 }),
407 },
408 } as OCPP20BootNotificationRequest;
409 }
410 };
411
412 export const warnTemplateKeysDeprecation = (
413 stationTemplate: ChargingStationTemplate,
414 logPrefix: string,
415 templateFile: string,
416 ) => {
417 const templateKeys: { deprecatedKey: string; key?: string }[] = [
418 { deprecatedKey: 'supervisionUrl', key: 'supervisionUrls' },
419 { deprecatedKey: 'authorizationFile', key: 'idTagsFile' },
420 { deprecatedKey: 'payloadSchemaValidation', key: 'ocppStrictCompliance' },
421 { deprecatedKey: 'mustAuthorizeAtRemoteStart', key: 'remoteAuthorization' },
422 ];
423 for (const templateKey of templateKeys) {
424 warnDeprecatedTemplateKey(
425 stationTemplate,
426 templateKey.deprecatedKey,
427 logPrefix,
428 templateFile,
429 !isUndefined(templateKey.key) ? `Use '${templateKey.key}' instead` : undefined,
430 );
431 convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key);
432 }
433 };
434
435 export const stationTemplateToStationInfo = (
436 stationTemplate: ChargingStationTemplate,
437 ): ChargingStationInfo => {
438 stationTemplate = cloneObject<ChargingStationTemplate>(stationTemplate);
439 delete stationTemplate.power;
440 delete stationTemplate.powerUnit;
441 delete stationTemplate.Connectors;
442 delete stationTemplate.Evses;
443 delete stationTemplate.Configuration;
444 delete stationTemplate.AutomaticTransactionGenerator;
445 delete stationTemplate.chargeBoxSerialNumberPrefix;
446 delete stationTemplate.chargePointSerialNumberPrefix;
447 delete stationTemplate.meterSerialNumberPrefix;
448 return stationTemplate as ChargingStationInfo;
449 };
450
451 export const createSerialNumber = (
452 stationTemplate: ChargingStationTemplate,
453 stationInfo: ChargingStationInfo,
454 params: {
455 randomSerialNumberUpperCase?: boolean;
456 randomSerialNumber?: boolean;
457 } = {
458 randomSerialNumberUpperCase: true,
459 randomSerialNumber: true,
460 },
461 ): void => {
462 params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params };
463 const serialNumberSuffix = params?.randomSerialNumber
464 ? getRandomSerialNumberSuffix({
465 upperCase: params.randomSerialNumberUpperCase,
466 })
467 : '';
468 isNotEmptyString(stationTemplate?.chargePointSerialNumberPrefix) &&
469 (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
470 isNotEmptyString(stationTemplate?.chargeBoxSerialNumberPrefix) &&
471 (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
472 isNotEmptyString(stationTemplate?.meterSerialNumberPrefix) &&
473 (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
474 };
475
476 export const propagateSerialNumber = (
477 stationTemplate: ChargingStationTemplate,
478 stationInfoSrc: ChargingStationInfo,
479 stationInfoDst: ChargingStationInfo,
480 ) => {
481 if (!stationInfoSrc || !stationTemplate) {
482 throw new BaseError(
483 'Missing charging station template or existing configuration to propagate serial number',
484 );
485 }
486 stationTemplate?.chargePointSerialNumberPrefix && stationInfoSrc?.chargePointSerialNumber
487 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
488 : stationInfoDst?.chargePointSerialNumber && delete stationInfoDst.chargePointSerialNumber;
489 stationTemplate?.chargeBoxSerialNumberPrefix && stationInfoSrc?.chargeBoxSerialNumber
490 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
491 : stationInfoDst?.chargeBoxSerialNumber && delete stationInfoDst.chargeBoxSerialNumber;
492 stationTemplate?.meterSerialNumberPrefix && stationInfoSrc?.meterSerialNumber
493 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
494 : stationInfoDst?.meterSerialNumber && delete stationInfoDst.meterSerialNumber;
495 };
496
497 export const hasFeatureProfile = (
498 chargingStation: ChargingStation,
499 featureProfile: SupportedFeatureProfiles,
500 ): boolean | undefined => {
501 return getConfigurationKey(
502 chargingStation,
503 StandardParametersKey.SupportedFeatureProfiles,
504 )?.value?.includes(featureProfile);
505 };
506
507 export const getAmperageLimitationUnitDivider = (stationInfo: ChargingStationInfo): number => {
508 let unitDivider = 1;
509 switch (stationInfo.amperageLimitationUnit) {
510 case AmpereUnits.DECI_AMPERE:
511 unitDivider = 10;
512 break;
513 case AmpereUnits.CENTI_AMPERE:
514 unitDivider = 100;
515 break;
516 case AmpereUnits.MILLI_AMPERE:
517 unitDivider = 1000;
518 break;
519 }
520 return unitDivider;
521 };
522
523 /**
524 * Gets the connector cloned charging profiles applying a power limitation
525 * and sorted by connector id descending then stack level descending
526 *
527 * @param chargingStation -
528 * @param connectorId -
529 * @returns connector charging profiles array
530 */
531 export const getConnectorChargingProfiles = (
532 chargingStation: ChargingStation,
533 connectorId: number,
534 ) => {
535 return cloneObject<ChargingProfile[]>(
536 (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? [])
537 .sort((a, b) => b.stackLevel - a.stackLevel)
538 .concat(
539 (chargingStation.getConnectorStatus(0)?.chargingProfiles ?? []).sort(
540 (a, b) => b.stackLevel - a.stackLevel,
541 ),
542 ),
543 );
544 };
545
546 export const getChargingStationConnectorChargingProfilesPowerLimit = (
547 chargingStation: ChargingStation,
548 connectorId: number,
549 ): number | undefined => {
550 let limit: number | undefined, chargingProfile: ChargingProfile | undefined;
551 // Get charging profiles sorted by connector id then stack level
552 const chargingProfiles = getConnectorChargingProfiles(chargingStation, connectorId);
553 if (isNotEmptyArray(chargingProfiles)) {
554 const result = getLimitFromChargingProfiles(
555 chargingStation,
556 connectorId,
557 chargingProfiles,
558 chargingStation.logPrefix(),
559 );
560 if (!isNullOrUndefined(result)) {
561 limit = result?.limit;
562 chargingProfile = result?.chargingProfile;
563 switch (chargingStation.getCurrentOutType()) {
564 case CurrentType.AC:
565 limit =
566 chargingProfile?.chargingSchedule?.chargingRateUnit === ChargingRateUnitType.WATT
567 ? limit
568 : ACElectricUtils.powerTotal(
569 chargingStation.getNumberOfPhases(),
570 chargingStation.getVoltageOut(),
571 limit!,
572 );
573 break;
574 case CurrentType.DC:
575 limit =
576 chargingProfile?.chargingSchedule?.chargingRateUnit === ChargingRateUnitType.WATT
577 ? limit
578 : DCElectricUtils.power(chargingStation.getVoltageOut(), limit!);
579 }
580 const connectorMaximumPower =
581 chargingStation.getMaximumPower() / chargingStation.powerDivider;
582 if (limit! > connectorMaximumPower) {
583 logger.error(
584 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${chargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
585 result,
586 );
587 limit = connectorMaximumPower;
588 }
589 }
590 }
591 return limit;
592 };
593
594 export const getDefaultVoltageOut = (
595 currentType: CurrentType,
596 logPrefix: string,
597 templateFile: string,
598 ): Voltage => {
599 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
600 let defaultVoltageOut: number;
601 switch (currentType) {
602 case CurrentType.AC:
603 defaultVoltageOut = Voltage.VOLTAGE_230;
604 break;
605 case CurrentType.DC:
606 defaultVoltageOut = Voltage.VOLTAGE_400;
607 break;
608 default:
609 logger.error(`${logPrefix} ${errorMsg}`);
610 throw new BaseError(errorMsg);
611 }
612 return defaultVoltageOut;
613 };
614
615 export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
616 return (
617 stationInfo.idTagsFile &&
618 join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
619 );
620 };
621
622 export const waitChargingStationEvents = async (
623 emitter: EventEmitter,
624 event: ChargingStationWorkerMessageEvents,
625 eventsToWait: number,
626 ): Promise<number> => {
627 return new Promise<number>((resolve) => {
628 let events = 0;
629 if (eventsToWait === 0) {
630 resolve(events);
631 }
632 emitter.on(event, () => {
633 ++events;
634 if (events === eventsToWait) {
635 resolve(events);
636 }
637 });
638 });
639 };
640
641 const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
642 let configuredMaxNumberOfConnectors = 0;
643 if (isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
644 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
645 configuredMaxNumberOfConnectors =
646 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)];
647 } else if (isUndefined(stationTemplate.numberOfConnectors) === false) {
648 configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors as number;
649 } else if (stationTemplate.Connectors && !stationTemplate.Evses) {
650 configuredMaxNumberOfConnectors = stationTemplate.Connectors[0]
651 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
652 : getMaxNumberOfConnectors(stationTemplate.Connectors);
653 } else if (stationTemplate.Evses && !stationTemplate.Connectors) {
654 for (const evse in stationTemplate.Evses) {
655 if (evse === '0') {
656 continue;
657 }
658 configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
659 stationTemplate.Evses[evse].Connectors,
660 );
661 }
662 }
663 return configuredMaxNumberOfConnectors;
664 };
665
666 const checkConfiguredMaxConnectors = (
667 configuredMaxConnectors: number,
668 logPrefix: string,
669 templateFile: string,
670 ): void => {
671 if (configuredMaxConnectors <= 0) {
672 logger.warn(
673 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
674 );
675 }
676 };
677
678 const checkTemplateMaxConnectors = (
679 templateMaxConnectors: number,
680 logPrefix: string,
681 templateFile: string,
682 ): void => {
683 if (templateMaxConnectors === 0) {
684 logger.warn(
685 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
686 );
687 } else if (templateMaxConnectors < 0) {
688 logger.error(
689 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
690 );
691 }
692 };
693
694 const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
695 connectorStatus.availability = AvailabilityType.Operative;
696 connectorStatus.idTagLocalAuthorized = false;
697 connectorStatus.idTagAuthorized = false;
698 connectorStatus.transactionRemoteStarted = false;
699 connectorStatus.transactionStarted = false;
700 connectorStatus.energyActiveImportRegisterValue = 0;
701 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
702 if (isUndefined(connectorStatus.chargingProfiles)) {
703 connectorStatus.chargingProfiles = [];
704 }
705 };
706
707 const warnDeprecatedTemplateKey = (
708 template: ChargingStationTemplate,
709 key: string,
710 logPrefix: string,
711 templateFile: string,
712 logMsgToAppend = '',
713 ): void => {
714 if (!isUndefined(template[key as keyof ChargingStationTemplate])) {
715 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
716 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
717 }`;
718 logger.warn(`${logPrefix} ${logMsg}`);
719 console.warn(chalk.yellow(`${logMsg}`));
720 }
721 };
722
723 const convertDeprecatedTemplateKey = (
724 template: ChargingStationTemplate,
725 deprecatedKey: string,
726 key?: string,
727 ): void => {
728 if (!isUndefined(template[deprecatedKey as keyof ChargingStationTemplate])) {
729 if (!isUndefined(key)) {
730 (template as unknown as Record<string, unknown>)[key!] =
731 template[deprecatedKey as keyof ChargingStationTemplate];
732 }
733 delete template[deprecatedKey as keyof ChargingStationTemplate];
734 }
735 };
736
737 interface ChargingProfilesLimit {
738 limit: number;
739 chargingProfile: ChargingProfile;
740 }
741
742 /**
743 * Charging profiles shall already be sorted by connector id descending then stack level descending
744 *
745 * @param chargingStation -
746 * @param connectorId -
747 * @param chargingProfiles -
748 * @param logPrefix -
749 * @returns ChargingProfilesLimit
750 */
751 const getLimitFromChargingProfiles = (
752 chargingStation: ChargingStation,
753 connectorId: number,
754 chargingProfiles: ChargingProfile[],
755 logPrefix: string,
756 ): ChargingProfilesLimit | undefined => {
757 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
758 const currentDate = new Date();
759 const connectorStatus = chargingStation.getConnectorStatus(connectorId)!;
760 for (const chargingProfile of chargingProfiles) {
761 const chargingSchedule = chargingProfile.chargingSchedule;
762 if (isNullOrUndefined(chargingSchedule?.startSchedule) && connectorStatus?.transactionStarted) {
763 logger.debug(
764 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`,
765 );
766 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
767 chargingSchedule.startSchedule = connectorStatus?.transactionStart;
768 }
769 if (
770 !isNullOrUndefined(chargingSchedule?.startSchedule) &&
771 !isDate(chargingSchedule?.startSchedule)
772 ) {
773 logger.warn(
774 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`,
775 );
776 chargingSchedule.startSchedule = convertToDate(chargingSchedule?.startSchedule)!;
777 }
778 if (
779 !isNullOrUndefined(chargingSchedule?.startSchedule) &&
780 isNullOrUndefined(chargingSchedule?.duration)
781 ) {
782 logger.debug(
783 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`,
784 );
785 // OCPP specifies that if duration is not defined, it should be infinite
786 chargingSchedule.duration = differenceInSeconds(maxTime, chargingSchedule.startSchedule!);
787 }
788 if (!prepareChargingProfileKind(connectorStatus, chargingProfile, currentDate, logPrefix)) {
789 continue;
790 }
791 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
792 continue;
793 }
794 // Check if the charging profile is active
795 if (
796 isWithinInterval(currentDate, {
797 start: chargingSchedule.startSchedule!,
798 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
799 })
800 ) {
801 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
802 const chargingSchedulePeriodCompareFn = (
803 a: ChargingSchedulePeriod,
804 b: ChargingSchedulePeriod,
805 ) => a.startPeriod - b.startPeriod;
806 if (
807 !isArraySorted<ChargingSchedulePeriod>(
808 chargingSchedule.chargingSchedulePeriod,
809 chargingSchedulePeriodCompareFn,
810 )
811 ) {
812 logger.warn(
813 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
814 );
815 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn);
816 }
817 // Check if the first schedule period startPeriod property is equal to 0
818 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
819 logger.error(
820 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`,
821 );
822 continue;
823 }
824 // Handle only one schedule period
825 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
826 const result: ChargingProfilesLimit = {
827 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
828 chargingProfile,
829 };
830 logger.debug(debugLogMsg, result);
831 return result;
832 }
833 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined;
834 // Search for the right schedule period
835 for (const [
836 index,
837 chargingSchedulePeriod,
838 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
839 // Find the right schedule period
840 if (
841 isAfter(
842 addSeconds(chargingSchedule.startSchedule!, chargingSchedulePeriod.startPeriod),
843 currentDate,
844 )
845 ) {
846 // Found the schedule period: previous is the correct one
847 const result: ChargingProfilesLimit = {
848 limit: previousChargingSchedulePeriod!.limit,
849 chargingProfile,
850 };
851 logger.debug(debugLogMsg, result);
852 return result;
853 }
854 // Keep a reference to previous one
855 previousChargingSchedulePeriod = chargingSchedulePeriod;
856 // Handle the last schedule period within the charging profile duration
857 if (
858 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
859 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
860 differenceInSeconds(
861 addSeconds(
862 chargingSchedule.startSchedule!,
863 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
864 ),
865 chargingSchedule.startSchedule!,
866 ) > chargingSchedule.duration!)
867 ) {
868 const result: ChargingProfilesLimit = {
869 limit: previousChargingSchedulePeriod.limit,
870 chargingProfile,
871 };
872 logger.debug(debugLogMsg, result);
873 return result;
874 }
875 }
876 }
877 }
878 }
879 };
880
881 export const prepareChargingProfileKind = (
882 connectorStatus: ConnectorStatus,
883 chargingProfile: ChargingProfile,
884 currentDate: Date,
885 logPrefix: string,
886 ): boolean => {
887 switch (chargingProfile.chargingProfileKind) {
888 case ChargingProfileKindType.RECURRING:
889 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
890 return false;
891 }
892 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix);
893 break;
894 case ChargingProfileKindType.RELATIVE:
895 if (!isNullOrUndefined(chargingProfile.chargingSchedule.startSchedule)) {
896 logger.warn(
897 `${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`,
898 );
899 delete chargingProfile.chargingSchedule.startSchedule;
900 }
901 if (connectorStatus?.transactionStarted) {
902 chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart;
903 }
904 // FIXME: Handle relative charging profile duration
905 break;
906 }
907 return true;
908 };
909
910 export const canProceedChargingProfile = (
911 chargingProfile: ChargingProfile,
912 currentDate: Date,
913 logPrefix: string,
914 ): boolean => {
915 if (
916 (isValidTime(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom!)) ||
917 (isValidTime(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo!))
918 ) {
919 logger.debug(
920 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
921 chargingProfile.chargingProfileId
922 } is not valid for the current date ${currentDate.toISOString()}`,
923 );
924 return false;
925 }
926 if (
927 isNullOrUndefined(chargingProfile.chargingSchedule.startSchedule) ||
928 isNullOrUndefined(chargingProfile.chargingSchedule.duration)
929 ) {
930 logger.error(
931 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`,
932 );
933 return false;
934 }
935 if (
936 !isNullOrUndefined(chargingProfile.chargingSchedule.startSchedule) &&
937 !isValidTime(chargingProfile.chargingSchedule.startSchedule)
938 ) {
939 logger.error(
940 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`,
941 );
942 return false;
943 }
944 if (
945 !isNullOrUndefined(chargingProfile.chargingSchedule.duration) &&
946 !Number.isSafeInteger(chargingProfile.chargingSchedule.duration)
947 ) {
948 logger.error(
949 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`,
950 );
951 return false;
952 }
953 return true;
954 };
955
956 const canProceedRecurringChargingProfile = (
957 chargingProfile: ChargingProfile,
958 logPrefix: string,
959 ): boolean => {
960 if (
961 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
962 isNullOrUndefined(chargingProfile.recurrencyKind)
963 ) {
964 logger.error(
965 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
966 );
967 return false;
968 }
969 if (isNullOrUndefined(chargingProfile.chargingSchedule.startSchedule)) {
970 logger.error(
971 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`,
972 );
973 return false;
974 }
975 return true;
976 };
977
978 /**
979 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
980 *
981 * @param chargingProfile -
982 * @param currentDate -
983 * @param logPrefix -
984 */
985 const prepareRecurringChargingProfile = (
986 chargingProfile: ChargingProfile,
987 currentDate: Date,
988 logPrefix: string,
989 ): boolean => {
990 const chargingSchedule = chargingProfile.chargingSchedule;
991 let recurringIntervalTranslated = false;
992 let recurringInterval: Interval;
993 switch (chargingProfile.recurrencyKind) {
994 case RecurrencyKindType.DAILY:
995 recurringInterval = {
996 start: chargingSchedule.startSchedule!,
997 end: addDays(chargingSchedule.startSchedule!, 1),
998 };
999 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
1000 if (
1001 !isWithinInterval(currentDate, recurringInterval) &&
1002 isBefore(recurringInterval.end, currentDate)
1003 ) {
1004 chargingSchedule.startSchedule = addDays(
1005 recurringInterval.start,
1006 differenceInDays(currentDate, recurringInterval.start),
1007 );
1008 recurringInterval = {
1009 start: chargingSchedule.startSchedule,
1010 end: addDays(chargingSchedule.startSchedule, 1),
1011 };
1012 recurringIntervalTranslated = true;
1013 }
1014 break;
1015 case RecurrencyKindType.WEEKLY:
1016 recurringInterval = {
1017 start: chargingSchedule.startSchedule!,
1018 end: addWeeks(chargingSchedule.startSchedule!, 1),
1019 };
1020 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
1021 if (
1022 !isWithinInterval(currentDate, recurringInterval) &&
1023 isBefore(recurringInterval.end, currentDate)
1024 ) {
1025 chargingSchedule.startSchedule = addWeeks(
1026 recurringInterval.start,
1027 differenceInWeeks(currentDate, recurringInterval.start),
1028 );
1029 recurringInterval = {
1030 start: chargingSchedule.startSchedule,
1031 end: addWeeks(chargingSchedule.startSchedule, 1),
1032 };
1033 recurringIntervalTranslated = true;
1034 }
1035 break;
1036 default:
1037 logger.error(
1038 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`,
1039 );
1040 }
1041 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
1042 logger.error(
1043 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1044 chargingProfile.recurrencyKind
1045 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1046 recurringInterval!.start,
1047 ).toISOString()}, ${toDate(
1048 recurringInterval!.end,
1049 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
1050 );
1051 }
1052 return recurringIntervalTranslated;
1053 };
1054
1055 const checkRecurringChargingProfileDuration = (
1056 chargingProfile: ChargingProfile,
1057 interval: Interval,
1058 logPrefix: string,
1059 ): void => {
1060 if (isNullOrUndefined(chargingProfile.chargingSchedule.duration)) {
1061 logger.warn(
1062 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1063 chargingProfile.chargingProfileKind
1064 } charging profile id ${
1065 chargingProfile.chargingProfileId
1066 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1067 interval.end,
1068 interval.start,
1069 )}`,
1070 );
1071 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
1072 } else if (
1073 chargingProfile.chargingSchedule.duration! > differenceInSeconds(interval.end, interval.start)
1074 ) {
1075 logger.warn(
1076 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1077 chargingProfile.chargingProfileKind
1078 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1079 chargingProfile.chargingSchedule.duration
1080 } is greater than the recurrency time interval duration ${differenceInSeconds(
1081 interval.end,
1082 interval.start,
1083 )}`,
1084 );
1085 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
1086 }
1087 };
1088
1089 const getRandomSerialNumberSuffix = (params?: {
1090 randomBytesLength?: number;
1091 upperCase?: boolean;
1092 }): string => {
1093 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex');
1094 if (params?.upperCase) {
1095 return randomSerialNumberSuffix.toUpperCase();
1096 }
1097 return randomSerialNumberSuffix;
1098 };