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