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