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