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